From 1eef758ecc6a59ea983e953d775a7eac30fbe542 Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 11 Sep 2019 16:51:05 -0400 Subject: [PATCH 001/296] Add support for DOODS Image Processing (#26208) * Add support for doods * Move connection to external module * Fix for CI * Another update for CI * Reformatted via black * Updated linting stuff * Updated per code review * Removed none check for something with a default * Updated config parsing * Updated if statements, need to disable lint check * Fixed formatting and bug that should make linter happy * Fixed one more issue with box drawing for areas * removed extra imports * Reworked per suggestion * Changed output to debug for informational detection message --- .coveragerc | 1 + homeassistant/components/doods/__init__.py | 1 + .../components/doods/image_processing.py | 360 ++++++++++++++++++ homeassistant/components/doods/manifest.json | 10 + requirements_all.txt | 3 + 5 files changed, 375 insertions(+) create mode 100644 homeassistant/components/doods/__init__.py create mode 100644 homeassistant/components/doods/image_processing.py create mode 100644 homeassistant/components/doods/manifest.json diff --git a/.coveragerc b/.coveragerc index ad001e5604839e..0c6ac82894a6f4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -143,6 +143,7 @@ omit = homeassistant/components/dlna_dmr/media_player.py homeassistant/components/dnsip/sensor.py homeassistant/components/dominos/* + homeassistant/components/doods/* homeassistant/components/doorbird/* homeassistant/components/dovado/* homeassistant/components/downloader/* diff --git a/homeassistant/components/doods/__init__.py b/homeassistant/components/doods/__init__.py new file mode 100644 index 00000000000000..b6edb9be87bda0 --- /dev/null +++ b/homeassistant/components/doods/__init__.py @@ -0,0 +1 @@ +"""The doods component.""" diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py new file mode 100644 index 00000000000000..ba44d86c2e4083 --- /dev/null +++ b/homeassistant/components/doods/image_processing.py @@ -0,0 +1,360 @@ +"""Support for the DOODS service.""" +import io +import logging +import time + +import voluptuous as vol +from PIL import Image, ImageDraw +from pydoods import PyDOODS + +from homeassistant.components.image_processing import ( + CONF_CONFIDENCE, + CONF_ENTITY_ID, + CONF_NAME, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingEntity, +) +from homeassistant.core import split_entity_id +from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +ATTR_MATCHES = "matches" +ATTR_SUMMARY = "summary" +ATTR_TOTAL_MATCHES = "total_matches" + +CONF_URL = "url" +CONF_AUTH_KEY = "auth_key" +CONF_DETECTOR = "detector" +CONF_LABELS = "labels" +CONF_AREA = "area" +CONF_TOP = "top" +CONF_BOTTOM = "bottom" +CONF_RIGHT = "right" +CONF_LEFT = "left" +CONF_FILE_OUT = "file_out" + +AREA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_BOTTOM, default=1): cv.small_float, + vol.Optional(CONF_LEFT, default=0): cv.small_float, + vol.Optional(CONF_RIGHT, default=1): cv.small_float, + vol.Optional(CONF_TOP, default=0): cv.small_float, + } +) + +LABEL_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_AREA): AREA_SCHEMA, + vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100), + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_URL): cv.string, + vol.Required(CONF_DETECTOR): cv.string, + vol.Optional(CONF_AUTH_KEY, default=""): cv.string, + vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]), + vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100), + vol.Optional(CONF_LABELS, default=[]): vol.All( + cv.ensure_list, [vol.Any(cv.string, LABEL_SCHEMA)] + ), + vol.Optional(CONF_AREA): AREA_SCHEMA, + } +) + + +def draw_box(draw, box, img_width, img_height, text="", color=(255, 255, 0)): + """Draw bounding box on image.""" + ymin, xmin, ymax, xmax = box + (left, right, top, bottom) = ( + xmin * img_width, + xmax * img_width, + ymin * img_height, + ymax * img_height, + ) + draw.line( + [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], + width=5, + fill=color, + ) + if text: + draw.text((left, abs(top - 15)), text, fill=color) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Doods client.""" + url = config[CONF_URL] + auth_key = config[CONF_AUTH_KEY] + detector_name = config[CONF_DETECTOR] + + doods = PyDOODS(url, auth_key) + response = doods.get_detectors() + if not isinstance(response, dict): + _LOGGER.warning("Could not connect to doods server: %s", url) + return + + detector = {} + for server_detector in response["detectors"]: + if server_detector["name"] == detector_name: + detector = server_detector + break + + if not detector: + _LOGGER.warning( + "Detector %s is not supported by doods server %s", detector_name, url + ) + return + + entities = [] + for camera in config[CONF_SOURCE]: + entities.append( + Doods( + hass, + camera[CONF_ENTITY_ID], + camera.get(CONF_NAME), + doods, + detector, + config, + ) + ) + add_entities(entities) + + +class Doods(ImageProcessingEntity): + """Doods image processing service client.""" + + def __init__(self, hass, camera_entity, name, doods, detector, config): + """Initialize the DOODS entity.""" + self.hass = hass + self._camera_entity = camera_entity + if name: + self._name = name + else: + name = split_entity_id(camera_entity)[1] + self._name = f"Doods {name}" + self._doods = doods + self._file_out = config[CONF_FILE_OUT] + + # detector config and aspect ratio + self._width = None + self._height = None + self._aspect = None + if detector["width"] and detector["height"]: + self._width = detector["width"] + self._height = detector["height"] + self._aspect = self._width / self._height + + # the base confidence + dconfig = {} + confidence = config[CONF_CONFIDENCE] + + # handle labels and specific detection areas + labels = config[CONF_LABELS] + self._label_areas = {} + for label in labels: + if isinstance(label, dict): + label_name = label[CONF_NAME] + if label_name not in detector["labels"] and label_name != "*": + _LOGGER.warning("Detector does not support label %s", label_name) + continue + + # Label Confidence + label_confidence = label[CONF_CONFIDENCE] + if label_name not in dconfig or dconfig[label_name] > label_confidence: + dconfig[label_name] = label_confidence + + # Label area + label_area = label.get(CONF_AREA) + self._label_areas[label_name] = [0, 0, 1, 1] + if label_area: + self._label_areas[label_name] = [ + label_area[CONF_TOP], + label_area[CONF_LEFT], + label_area[CONF_BOTTOM], + label_area[CONF_RIGHT], + ] + else: + if label not in detector["labels"] and label != "*": + _LOGGER.warning("Detector does not support label %s", label) + continue + self._label_areas[label] = [0, 0, 1, 1] + if label not in dconfig or dconfig[label] > confidence: + dconfig[label] = confidence + + if not dconfig: + dconfig["*"] = confidence + + # Handle global detection area + self._area = [0, 0, 1, 1] + area_config = config.get(CONF_AREA) + if area_config: + self._area = [ + area_config[CONF_TOP], + area_config[CONF_LEFT], + area_config[CONF_BOTTOM], + area_config[CONF_RIGHT], + ] + + template.attach(hass, self._file_out) + + self._dconfig = dconfig + self._matches = {} + self._total_matches = 0 + self._last_image = None + + @property + def camera_entity(self): + """Return camera entity id from process pictures.""" + return self._camera_entity + + @property + def name(self): + """Return the name of the image processor.""" + return self._name + + @property + def state(self): + """Return the state of the entity.""" + return self._total_matches + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return { + ATTR_MATCHES: self._matches, + ATTR_SUMMARY: { + label: len(values) for label, values in self._matches.items() + }, + ATTR_TOTAL_MATCHES: self._total_matches, + } + + def _save_image(self, image, matches, paths): + img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") + img_width, img_height = img.size + draw = ImageDraw.Draw(img) + + # Draw custom global region/area + if self._area != [0, 0, 1, 1]: + draw_box( + draw, self._area, img_width, img_height, "Detection Area", (0, 255, 255) + ) + + for label, values in matches.items(): + + # Draw custom label regions/areas + if label in self._label_areas and self._label_areas[label] != [0, 0, 1, 1]: + box_label = f"{label.capitalize()} Detection Area" + draw_box( + draw, + self._label_areas[label], + img_width, + img_height, + box_label, + (0, 255, 0), + ) + + # Draw detected objects + for instance in values: + box_label = f'{label} {instance["score"]:.1f}%' + # Already scaled, use 1 for width and height + draw_box( + draw, + instance["box"], + img_width, + img_height, + box_label, + (255, 255, 0), + ) + + for path in paths: + _LOGGER.info("Saving results image to %s", path) + img.save(path) + + def process_image(self, image): + """Process the image.""" + img = Image.open(io.BytesIO(bytearray(image))) + img_width, img_height = img.size + + if self._aspect and abs((img_width / img_height) - self._aspect) > 0.1: + _LOGGER.debug( + "The image aspect: %s and the detector aspect: %s differ by more than 0.1", + (img_width / img_height), + self._aspect, + ) + + # Run detection + start = time.time() + response = self._doods.detect(image, self._dconfig) + _LOGGER.debug( + "doods detect: %s response: %s duration: %s", + self._dconfig, + response, + time.time() - start, + ) + + matches = {} + total_matches = 0 + + if not response or "error" in response: + if "error" in response: + _LOGGER.error(response["error"]) + self._matches = matches + self._total_matches = total_matches + return + + for detection in response["detections"]: + score = detection["confidence"] + boxes = [ + detection["top"], + detection["left"], + detection["bottom"], + detection["right"], + ] + label = detection["label"] + + # Exclude unlisted labels + if "*" not in self._dconfig and label not in self._dconfig: + continue + + # Exclude matches outside global area definition + if ( + boxes[0] < self._area[0] + or boxes[1] < self._area[1] + or boxes[2] > self._area[2] + or boxes[3] > self._area[3] + ): + continue + + # Exclude matches outside label specific area definition + if self._label_areas and ( + boxes[0] < self._label_areas[label][0] + or boxes[1] < self._label_areas[label][1] + or boxes[2] > self._label_areas[label][2] + or boxes[3] > self._label_areas[label][3] + ): + continue + + if label not in matches: + matches[label] = [] + matches[label].append({"score": float(score), "box": boxes}) + total_matches += 1 + + # Save Images + if total_matches and self._file_out: + paths = [] + for path_template in self._file_out: + if isinstance(path_template, template.Template): + paths.append( + path_template.render(camera_entity=self._camera_entity) + ) + else: + paths.append(path_template) + self._save_image(image, matches, paths) + + self._matches = matches + self._total_matches = total_matches diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json new file mode 100644 index 00000000000000..3e1ce22a230b03 --- /dev/null +++ b/homeassistant/components/doods/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "doods", + "name": "DOODS - Distributed Outside Object Detection Service", + "documentation": "https://www.home-assistant.io/components/doods", + "requirements": [ + "pydoods==1.0.1" + ], + "dependencies": [], + "codeowners": [] +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index da36928d6ba834..8a4bb391da452f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1142,6 +1142,9 @@ pydelijn==0.5.1 # homeassistant.components.zwave pydispatcher==2.0.5 +# homeassistant.components.doods +pydoods==1.0.1 + # homeassistant.components.android_ip_webcam pydroid-ipcam==0.8 From fc21bdbe273fd15860ff749b824bad78f56177e8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Sep 2019 17:27:56 -0600 Subject: [PATCH 002/296] Update PyChromecast (#26594) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 4fb1c67a56e440..84a6a6e2934fd6 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/components/cast", - "requirements": ["pychromecast==4.0.0"], + "requirements": ["pychromecast==4.0.1"], "dependencies": [], "zeroconf": ["_googlecast._tcp.local."], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 8a4bb391da452f..ee42e82f97150a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1110,7 +1110,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==4.0.0 +pychromecast==4.0.1 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07adcfc79f6933..7125d3e9ed5a02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -279,7 +279,7 @@ pyMetno==0.4.6 pyblackbird==0.5 # homeassistant.components.cast -pychromecast==4.0.0 +pychromecast==4.0.1 # homeassistant.components.deconz pydeconz==62 From 3fda07a4eac66feba04c12fc50365007d7241fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Thu, 12 Sep 2019 02:03:02 +0200 Subject: [PATCH 003/296] Bump zigate to 0.3.0 (#26586) * Bump zigate to 0.3.0 --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 3095d14061919e..6a2543e8b24533 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.3.0", "zigpy-homeassistant==0.8.0", "zigpy-xbee-homeassistant==0.4.0", - "zigpy-zigate==0.2.0" + "zigpy-zigate==0.3.0" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index ee42e82f97150a..48fe33d9e58527 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2028,7 +2028,7 @@ zigpy-homeassistant==0.8.0 zigpy-xbee-homeassistant==0.4.0 # homeassistant.components.zha -zigpy-zigate==0.2.0 +zigpy-zigate==0.3.0 # homeassistant.components.zoneminder zm-py==0.3.3 From d4c5cf396790e89aedc4693a91cc984e7a335bac Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 12 Sep 2019 00:33:41 +0000 Subject: [PATCH 004/296] [ci skip] Translation update --- .../components/deconz/.translations/da.json | 18 +++++++++ .../components/deconz/.translations/fr.json | 40 +++++++++++++++++++ .../components/deconz/.translations/it.json | 29 ++++++++++++++ .../deconz/.translations/zh-Hant.json | 29 ++++++++++++++ .../iaqualink/.translations/fr.json | 7 ++++ .../components/light/.translations/fr.json | 9 +++++ .../light/.translations/zh-Hant.json | 14 +++---- .../solaredge/.translations/zh-Hant.json | 21 ++++++++++ .../components/switch/.translations/fr.json | 17 ++++++++ .../switch/.translations/zh-Hant.json | 17 ++++++++ .../components/velbus/.translations/fr.json | 13 ++++++ 11 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/iaqualink/.translations/fr.json create mode 100644 homeassistant/components/solaredge/.translations/zh-Hant.json create mode 100644 homeassistant/components/switch/.translations/fr.json create mode 100644 homeassistant/components/switch/.translations/zh-Hant.json create mode 100644 homeassistant/components/velbus/.translations/fr.json diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index 1b595924106cfd..6b74c09107a098 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -41,6 +41,24 @@ }, "title": "deCONZ Zigbee gateway" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Begge knapper", + "button_1": "F\u00f8rste knap", + "button_2": "Anden knap", + "button_3": "Tredje knap", + "button_4": "Fjerde knap", + "close": "Luk", + "dim_down": "D\u00e6mp ned", + "dim_up": "D\u00e6mp op", + "left": "Venstre", + "open": "\u00c5ben", + "right": "H\u00f8jre" + }, + "trigger_type": { + "remote_gyro_activated": "Enhed rystet" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 9b98914314a749..0f1277e0b0584b 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -40,5 +40,45 @@ } }, "title": "Passerelle deCONZ Zigbee" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Les deux boutons", + "button_1": "Premier bouton", + "button_2": "Deuxi\u00e8me bouton", + "button_3": "Troisi\u00e8me bouton", + "button_4": "Quatri\u00e8me bouton", + "close": "Ferm\u00e9", + "dim_down": "Assombrir", + "dim_up": "\u00c9claircir", + "left": "Gauche", + "open": "Ouvert", + "right": "Droite", + "turn_off": "\u00c9teint", + "turn_on": "Allum\u00e9" + }, + "trigger_type": { + "remote_button_double_press": "Bouton \"{subtype}\" double cliqu\u00e9", + "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", + "remote_button_long_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9 apr\u00e8s appui long", + "remote_button_quadruple_press": "Bouton \"{subtype}\" quadruple cliqu\u00e9", + "remote_button_quintuple_press": "Bouton \"{subtype}\" quintuple cliqu\u00e9", + "remote_button_rotated": "Bouton \"{subtype}\" tourn\u00e9", + "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", + "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", + "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", + "remote_gyro_activated": "Appareil secou\u00e9" + } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP", + "allow_deconz_groups": "Autoriser les groupes de lumi\u00e8res deCONZ" + }, + "description": "Configurer la visibilit\u00e9 des appareils de type deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 90b85aaeba5f01..f14e7b4c66797b 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -41,6 +41,35 @@ }, "title": "Gateway Zigbee deCONZ" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Entrambi i pulsanti", + "button_1": "Primo pulsante", + "button_2": "Secondo pulsante", + "button_3": "Terzo pulsante", + "button_4": "Quarto pulsante", + "close": "Chiudere", + "dim_down": "Diminuire luminosit\u00e0", + "dim_up": "Aumentare luminosit\u00e0", + "left": "Sinistra", + "open": "Aperto", + "right": "Destra", + "turn_off": "Spegnere", + "turn_on": "Accendere" + }, + "trigger_type": { + "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", + "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", + "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", + "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", + "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", + "remote_button_rotated": "Pulsante ruotato \"{subtype}\"", + "remote_button_short_press": "Pulsante \"{subtype}\" premuto", + "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", + "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", + "remote_gyro_activated": "Dispositivo in vibrazione" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 75dcac93dd9eb0..f024386aa0f873 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee \u9598\u9053\u5668" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u5169\u500b\u6309\u9215", + "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", + "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "close": "\u95dc\u9589", + "dim_down": "\u8abf\u6697", + "dim_up": "\u8abf\u4eae", + "left": "\u5de6", + "open": "\u958b\u555f", + "right": "\u53f3", + "turn_off": "\u95dc\u9589", + "turn_on": "\u958b\u555f" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", + "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", + "remote_button_long_release": "\u9577\u6309\u5f8c\u91cb\u653e \"{subtype}\" \u6309\u9215", + "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u9ede\u64ca", + "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u9ede\u64ca", + "remote_button_rotated": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215", + "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", + "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", + "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/iaqualink/.translations/fr.json b/homeassistant/components/iaqualink/.translations/fr.json new file mode 100644 index 00000000000000..cf449ebb358cbc --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'une seule connexion iAqualink." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json index 00d03b12d0130a..6ab87274409320 100644 --- a/homeassistant/components/light/.translations/fr.json +++ b/homeassistant/components/light/.translations/fr.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Basculer {entity_name}", + "turn_off": "\u00c9teindre {entity_name}", + "turn_on": "Allumer {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est \u00e9teint", + "is_on": "{entity_name} est allum\u00e9" + }, "trigger_type": { "turn_off": "{name} d\u00e9sactiv\u00e9", "turn_on": "{name} activ\u00e9" diff --git a/homeassistant/components/light/.translations/zh-Hant.json b/homeassistant/components/light/.translations/zh-Hant.json index 269715b7cc33fe..8f5fec9b309e82 100644 --- a/homeassistant/components/light/.translations/zh-Hant.json +++ b/homeassistant/components/light/.translations/zh-Hant.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "\u5207\u63db {name}", - "turn_off": "\u95dc\u9589 {name}", - "turn_on": "\u958b\u555f {name}" + "toggle": "\u5207\u63db {entity_name}", + "turn_off": "\u95dc\u9589 {entity_name}", + "turn_on": "\u958b\u555f {entity_name}" }, "condition_type": { - "is_off": "{name} \u5df2\u95dc\u9589", - "is_on": "{name} \u5df2\u958b\u555f" + "is_off": "{entity_name} \u5df2\u95dc\u9589", + "is_on": "{entity_name} \u5df2\u958b\u555f" }, "trigger_type": { - "turn_off": "\u7531 {name} \u95dc\u9589", - "turn_on": "\u7531 {name} \u958b\u555f" + "turn_off": "{entity_name} \u5df2\u95dc\u9589", + "turn_on": "{entity_name} \u5df2\u958b\u555f" } } } \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/zh-Hant.json b/homeassistant/components/solaredge/.translations/zh-Hant.json new file mode 100644 index 00000000000000..698c28d99bf72a --- /dev/null +++ b/homeassistant/components/solaredge/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "name": "\u5b89\u88dd\u540d\u7a31", + "site_id": "SolarEdge site-id" + }, + "title": "\u8a2d\u5b9a API \u53c3\u6578" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/fr.json b/homeassistant/components/switch/.translations/fr.json new file mode 100644 index 00000000000000..eeffc9262e56ea --- /dev/null +++ b/homeassistant/components/switch/.translations/fr.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Basculer {entity_name}", + "turn_off": "\u00c9teindre {entity_name}", + "turn_on": "Allumer {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} \u00e9teint", + "turn_on": "{entity_name} allum\u00e9" + }, + "trigger_type": { + "turn_off": "{entity_name} \u00e9teint", + "turn_on": "{entity_name} allum\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/zh-Hant.json b/homeassistant/components/switch/.translations/zh-Hant.json new file mode 100644 index 00000000000000..0607f4ab08ee3c --- /dev/null +++ b/homeassistant/components/switch/.translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u5207\u63db {entity_name}", + "turn_off": "\u95dc\u9589 {entity_name}", + "turn_on": "\u958b\u555f {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} \u5df2\u95dc\u9589", + "turn_on": "{entity_name} \u5df2\u958b\u555f" + }, + "trigger_type": { + "turn_off": "{entity_name} \u5df2\u95dc\u9589", + "turn_on": "{entity_name} \u5df2\u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/fr.json b/homeassistant/components/velbus/.translations/fr.json new file mode 100644 index 00000000000000..f930df12861abe --- /dev/null +++ b/homeassistant/components/velbus/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Le nom pour cette connexion velbus" + }, + "title": "D\u00e9finir le type de connexion velbus" + } + }, + "title": "Interface Velbus" + } +} \ No newline at end of file From c06487fa5e1b0c2b1d203201bb97b4396ead5113 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Thu, 12 Sep 2019 08:30:04 +0200 Subject: [PATCH 005/296] Upgrade youtube_dl to 2019.09.12.1 (#26593) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 419d4b72864400..4e253741b051f8 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,7 +3,7 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/components/media_extractor", "requirements": [ - "youtube_dl==2019.09.01" + "youtube_dl==2019.09.12.1" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 48fe33d9e58527..09b8ffb746a524 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2001,7 +2001,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.09.01 +youtube_dl==2019.09.12.1 # homeassistant.components.zengge zengge==0.2 From 63cf21296ccf303c5ada6b6d2c837744e4ac804d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 12 Sep 2019 08:30:28 +0200 Subject: [PATCH 006/296] Update azure-pipelines-release.yml --- azure-pipelines-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 63ce5b707cf6b2..7c88e615fa5baa 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -68,8 +68,8 @@ stages: - script: python setup.py sdist bdist_wheel displayName: 'Build package' - script: | - TWINE_USERNAME="$(twineUser)" - TWINE_PASSWORD="$(twinePassword)" + export TWINE_USERNAME="$(twineUser)" + export TWINE_PASSWORD="$(twinePassword)" twine upload dist/* --skip-existing displayName: 'Upload pypi' From 41f96a315ee2a0be3c866da2bb10fff49f21a139 Mon Sep 17 00:00:00 2001 From: Gerard Date: Thu, 12 Sep 2019 09:50:02 +0200 Subject: [PATCH 007/296] Fix CCM messages (#26589) --- .../bmw_connected_drive/binary_sensor.py | 13 +++++++------ .../components/bmw_connected_drive/sensor.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index c9cc9b2d33373f..c13de4559847ef 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -9,7 +9,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "lids": ["Doors", "opening", "mdi:car-door"], + "lids": ["Doors", "opening", "mdi:car-door-lock"], "windows": ["Windows", "opening", "mdi:car-door"], "door_lock_state": ["Door lock state", "safety", "mdi:car-key"], "lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"], @@ -122,8 +122,9 @@ def device_state_attributes(self): for report in vehicle_state.condition_based_services: result.update(self._format_cbs_report(report)) elif self._attribute == "check_control_messages": - check_control_messages = vehicle_state.has_check_control_messages - if check_control_messages: + check_control_messages = vehicle_state.check_control_messages + has_check_control_messages = vehicle_state.has_check_control_messages + if has_check_control_messages: cbs_list = [] for message in check_control_messages: cbs_list.append(message["ccmDescriptionShort"]) @@ -184,9 +185,9 @@ def _format_cbs_report(self, report): distance = round( self.hass.config.units.length(report.due_distance, LENGTH_KILOMETERS) ) - result[f"{service_type} distance"] = "{} {}".format( - distance, self.hass.config.units.length_unit - ) + result[ + f"{service_type} distance" + ] = f"{distance} {self.hass.config.units.length_unit}" return result def update_callback(self): diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 011908d54585e4..96d541b1955337 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -17,10 +17,10 @@ ATTR_TO_HA_METRIC = { "mileage": ["mdi:speedometer", LENGTH_KILOMETERS], - "remaining_range_total": ["mdi:ruler", LENGTH_KILOMETERS], - "remaining_range_electric": ["mdi:ruler", LENGTH_KILOMETERS], - "remaining_range_fuel": ["mdi:ruler", LENGTH_KILOMETERS], - "max_range_electric": ["mdi:ruler", LENGTH_KILOMETERS], + "remaining_range_total": ["mdi:map-marker-distance", LENGTH_KILOMETERS], + "remaining_range_electric": ["mdi:map-marker-distance", LENGTH_KILOMETERS], + "remaining_range_fuel": ["mdi:map-marker-distance", LENGTH_KILOMETERS], + "max_range_electric": ["mdi:map-marker-distance", LENGTH_KILOMETERS], "remaining_fuel": ["mdi:gas-station", VOLUME_LITERS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], @@ -28,10 +28,10 @@ ATTR_TO_HA_IMPERIAL = { "mileage": ["mdi:speedometer", LENGTH_MILES], - "remaining_range_total": ["mdi:ruler", LENGTH_MILES], - "remaining_range_electric": ["mdi:ruler", LENGTH_MILES], - "remaining_range_fuel": ["mdi:ruler", LENGTH_MILES], - "max_range_electric": ["mdi:ruler", LENGTH_MILES], + "remaining_range_total": ["mdi:map-marker-distance", LENGTH_MILES], + "remaining_range_electric": ["mdi:map-marker-distance", LENGTH_MILES], + "remaining_range_fuel": ["mdi:map-marker-distance", LENGTH_MILES], + "max_range_electric": ["mdi:map-marker-distance", LENGTH_MILES], "remaining_fuel": ["mdi:gas-station", VOLUME_GALLONS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], From c6a73e9ef7f7ea6c70924059a5ce305efd8f3ba8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 12 Sep 2019 13:28:48 +0200 Subject: [PATCH 008/296] Update azure-pipelines-wheels.yml --- azure-pipelines-wheels.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index eec3f678981b53..8c534a88d30457 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -45,7 +45,6 @@ jobs: requirement_files="requirements_wheels.txt requirements_diff.txt" for requirement_file in ${requirement_files}; do - sed -i "s|# pytradfri|pytradfri|g" ${requirement_file} sed -i "s|# pybluez|pybluez|g" ${requirement_file} sed -i "s|# bluepy|bluepy|g" ${requirement_file} sed -i "s|# beacontools|beacontools|g" ${requirement_file} @@ -63,9 +62,12 @@ jobs: sed -i "s|# homekit|homekit|g" ${requirement_file} sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file} sed -i "s|# decora|decora|g" ${requirement_file} + sed -i "s|# avion|avion|g" ${requirement_file} sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file} sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} + sed -i "s|# VL53L1X|VL53L1X|g" ${requirement_file} + sed -i "s|# bme680|bme680|g" ${requirement_file} done displayName: 'Prepare requirements files for Hass.io' From 284ae015603795445ed352cb4de53103df891d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Thu, 12 Sep 2019 14:00:58 +0200 Subject: [PATCH 009/296] Bump zigpy-zigate to 0.3.1 (#26600) * Bump zigpy-zigate to 0.3.1 Bump zigpy-zigate to 0.3.1 (fix Rpi.GPIO dependency) * Bump zigpy-zigate to 0.3.1 Bump zigpy-zigate to 0.3.1 (fix Rpi.GPIO dependency) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 6a2543e8b24533..e78661a04e534d 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.3.0", "zigpy-homeassistant==0.8.0", "zigpy-xbee-homeassistant==0.4.0", - "zigpy-zigate==0.3.0" + "zigpy-zigate==0.3.1" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index 09b8ffb746a524..2075dab9a371e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2028,7 +2028,7 @@ zigpy-homeassistant==0.8.0 zigpy-xbee-homeassistant==0.4.0 # homeassistant.components.zha -zigpy-zigate==0.3.0 +zigpy-zigate==0.3.1 # homeassistant.components.zoneminder zm-py==0.3.3 From 25ef4a156f8584b7bd69d71d4ec9efcb5533f916 Mon Sep 17 00:00:00 2001 From: Gilad Peleg Date: Thu, 12 Sep 2019 19:01:55 +0300 Subject: [PATCH 010/296] Improve bluetooth tracker device code (#26067) * Improve bluetooth device tracker code * Don't use set operations * Fix logging template interpolation * Warn if not tracking new devices and not devices to track * Updates due to CR * Fix pylint warning * Fix pylint import warning * Merge with dev --- .../bluetooth_tracker/device_tracker.py | 158 ++++++++++-------- 1 file changed, 92 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index e760f91070a163..8f01036da75911 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -1,25 +1,30 @@ """Tracking for bluetooth devices.""" import logging +from typing import List, Set, Tuple +# pylint: disable=import-error +import bluetooth +from bt_proximity import BluetoothRSSI import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.components.device_tracker.legacy import ( - YAML_DEVICES, - async_load_config, -) from homeassistant.components.device_tracker.const import ( - CONF_TRACK_NEW, CONF_SCAN_INTERVAL, - SCAN_INTERVAL, + CONF_TRACK_NEW, DEFAULT_TRACK_NEW, - SOURCE_TYPE_BLUETOOTH, DOMAIN, + SCAN_INTERVAL, + SOURCE_TYPE_BLUETOOTH, ) -import homeassistant.util.dt as dt_util +from homeassistant.components.device_tracker.legacy import ( + YAML_DEVICES, + async_load_config, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.async_ import run_coroutine_threadsafe +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -42,66 +47,86 @@ ) -def setup_scanner(hass, config, see, discovery_info=None): - """Set up the Bluetooth Scanner.""" - # pylint: disable=import-error - import bluetooth - from bt_proximity import BluetoothRSSI - - def see_device(mac, name, rssi=None): - """Mark a device as seen.""" - attributes = {} - if rssi is not None: - attributes["rssi"] = rssi - see( - mac=f"{BT_PREFIX}{mac}", - host_name=name, - attributes=attributes, - source_type=SOURCE_TYPE_BLUETOOTH, - ) - - device_id = config.get(CONF_DEVICE_ID) - - def discover_devices(): - """Discover Bluetooth devices.""" - result = bluetooth.discover_devices( - duration=8, - lookup_names=True, - flush_cache=True, - lookup_class=False, - device_id=device_id, - ) - _LOGGER.debug("Bluetooth devices discovered = %d", len(result)) - return result - - yaml_path = hass.config.path(YAML_DEVICES) - devs_to_track = [] - devs_donot_track = [] - - # Load all known devices. - # We just need the devices so set consider_home and home range - # to 0 +def is_bluetooth_device(device) -> bool: + """Check whether a device is a bluetooth device by its mac.""" + return device.mac and device.mac[:3].upper() == BT_PREFIX + + +def discover_devices(device_id: int) -> List[Tuple[str, str]]: + """Discover Bluetooth devices.""" + result = bluetooth.discover_devices( + duration=8, + lookup_names=True, + flush_cache=True, + lookup_class=False, + device_id=device_id, + ) + _LOGGER.debug("Bluetooth devices discovered = %d", len(result)) + return result + + +def see_device(see, mac: str, device_name: str, rssi=None) -> None: + """Mark a device as seen.""" + attributes = {} + if rssi is not None: + attributes["rssi"] = rssi + see( + mac=f"{BT_PREFIX}{mac}", + host_name=device_name, + attributes=attributes, + source_type=SOURCE_TYPE_BLUETOOTH, + ) + + +def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[str]]: + """ + Load all known devices. + + We just need the devices so set consider_home and home range to 0 + """ + yaml_path: str = hass.config.path(YAML_DEVICES) + devices_to_track: Set[str] = set() + devices_to_not_track: Set[str] = set() + for device in run_coroutine_threadsafe( async_load_config(yaml_path, hass, 0), hass.loop ).result(): # Check if device is a valid bluetooth device - if device.mac and device.mac[:3].upper() == BT_PREFIX: - if device.track: - devs_to_track.append(device.mac[3:]) - else: - devs_donot_track.append(device.mac[3:]) + if not is_bluetooth_device(device): + continue + + normalized_mac: str = device.mac[3:] + if device.track: + devices_to_track.add(normalized_mac) + else: + devices_to_not_track.add(normalized_mac) + + return devices_to_track, devices_to_not_track + + +def setup_scanner(hass: HomeAssistantType, config: dict, see, discovery_info=None): + """Set up the Bluetooth Scanner.""" + device_id: int = config.get(CONF_DEVICE_ID) + devices_to_track, devices_to_not_track = get_tracking_devices(hass) # If track new devices is true discover new devices on startup. - track_new = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) + track_new: bool = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) + _LOGGER.debug("Tracking new devices = %s", track_new) + + if not devices_to_track and not track_new: + _LOGGER.debug("No Bluetooth devices to track and not tracking new devices") + if track_new: - for dev in discover_devices(): - if dev[0] not in devs_to_track and dev[0] not in devs_donot_track: - devs_to_track.append(dev[0]) - see_device(dev[0], dev[1]) + for mac, device_name in discover_devices(device_id): + if mac not in devices_to_track and mac not in devices_to_not_track: + devices_to_track.add(mac) + see_device(see, mac, device_name) interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) request_rssi = config.get(CONF_REQUEST_RSSI, False) + if request_rssi: + _LOGGER.debug("Detecting RSSI for devices") def update_bluetooth(_): """Update Bluetooth and set timer for the next update.""" @@ -112,21 +137,22 @@ def update_bluetooth_once(): """Lookup Bluetooth device and update status.""" try: if track_new: - for dev in discover_devices(): - if dev[0] not in devs_to_track and dev[0] not in devs_donot_track: - devs_to_track.append(dev[0]) - for mac in devs_to_track: + for mac, device_name in discover_devices(device_id): + if mac not in devices_to_track and mac not in devices_to_not_track: + devices_to_track.add(mac) + + for mac in devices_to_track: _LOGGER.debug("Scanning %s", mac) - result = bluetooth.lookup_name(mac, timeout=5) + device_name = bluetooth.lookup_name(mac, timeout=5) rssi = None if request_rssi: client = BluetoothRSSI(mac) rssi = client.request_rssi() client.close() - if result is None: + if device_name is None: # Could not lookup device name continue - see_device(mac, result, rssi) + see_device(see, mac, device_name, rssi) except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") From 32a6a76d6ac64ba221b6c37344667e97f9920ffb Mon Sep 17 00:00:00 2001 From: PoofyTeddy <33599733+poofyteddy@users.noreply.github.com> Date: Thu, 12 Sep 2019 21:50:24 +0200 Subject: [PATCH 011/296] Disable Watson TTS Telemetry (#26253) --- homeassistant/components/watson_tts/tts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 0b7228fb568bcf..a30d08f31f3d9e 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -99,6 +99,7 @@ def get_engine(hass, config): supported_languages = list({s[:5] for s in SUPPORTED_VOICES}) default_voice = config[CONF_VOICE] output_format = config[CONF_OUTPUT_FORMAT] + service.set_default_headers({"x-watson-learning-opt-out": "true"}) return WatsonTTSProvider(service, supported_languages, default_voice, output_format) From 10f742d55258971c1ea5181aefe532b89b15523e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 13 Sep 2019 00:33:08 +0000 Subject: [PATCH 012/296] [ci skip] Translation update --- .../arcam_fmj/.translations/fr.json | 5 ++++ .../cert_expiry/.translations/fr.json | 24 +++++++++++++++ .../components/deconz/.translations/ca.json | 29 +++++++++++++++++++ .../components/deconz/.translations/fr.json | 7 +++++ .../components/deconz/.translations/it.json | 4 +-- .../components/deconz/.translations/ko.json | 29 +++++++++++++++++++ .../components/deconz/.translations/pl.json | 17 +++++++++++ .../components/deconz/.translations/ru.json | 28 ++++++++++++++++-- .../geonetnz_quakes/.translations/fr.json | 17 +++++++++++ .../iaqualink/.translations/fr.json | 16 +++++++++- .../components/life360/.translations/fr.json | 1 + .../solaredge/.translations/fr.json | 21 ++++++++++++++ .../components/traccar/.translations/fr.json | 18 ++++++++++++ .../twentemilieu/.translations/fr.json | 23 +++++++++++++++ .../components/unifi/.translations/fr.json | 12 ++++++++ .../components/velbus/.translations/fr.json | 10 ++++++- 16 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/fr.json create mode 100644 homeassistant/components/cert_expiry/.translations/fr.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/fr.json create mode 100644 homeassistant/components/solaredge/.translations/fr.json create mode 100644 homeassistant/components/traccar/.translations/fr.json create mode 100644 homeassistant/components/twentemilieu/.translations/fr.json diff --git a/homeassistant/components/arcam_fmj/.translations/fr.json b/homeassistant/components/arcam_fmj/.translations/fr.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/fr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/fr.json b/homeassistant/components/cert_expiry/.translations/fr.json new file mode 100644 index 00000000000000..a3536902c76d26 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e" + }, + "error": { + "certificate_fetch_failed": "Impossible de r\u00e9cup\u00e9rer le certificat de cette combinaison h\u00f4te / port", + "connection_timeout": "D\u00e9lai d'attente lors de la connexion \u00e0 cet h\u00f4te", + "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e", + "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu" + }, + "step": { + "user": { + "data": { + "host": "Le nom d'h\u00f4te du certificat", + "name": "Le nom du certificat", + "port": "Le port du certificat" + }, + "title": "D\u00e9finir le certificat \u00e0 tester" + } + }, + "title": "Expiration du certificat" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 263730ba5837c2..d36de4acc1e96c 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -41,6 +41,35 @@ }, "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Ambd\u00f3s botons", + "button_1": "Primer bot\u00f3", + "button_2": "Segon bot\u00f3", + "button_3": "Tercer bot\u00f3", + "button_4": "Quart bot\u00f3", + "close": "Tanca", + "dim_down": "Atenua la brillantor", + "dim_up": "Augmenta la brillantor", + "left": "Esquerra", + "open": "Obert", + "right": "Dreta", + "turn_off": "Desactiva", + "turn_on": "Activa" + }, + "trigger_type": { + "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives", + "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament", + "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives", + "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives", + "remote_button_rotated": "Bot\u00f3 \"{subtype}\" girat", + "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", + "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", + "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives", + "remote_gyro_activated": "Dispositiu sacsejat" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 0f1277e0b0584b..cc6d22945dcb79 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -72,6 +72,13 @@ }, "options": { "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP", + "allow_deconz_groups": "Autoriser les groupes de lumi\u00e8res deCONZ" + }, + "description": "Configurer la visibilit\u00e9 des appareils de type deCONZ" + }, "deconz_devices": { "data": { "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP", diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index f14e7b4c66797b..7a2b8832864e2b 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -43,8 +43,8 @@ }, "device_automation": { "trigger_subtype": { - "both_buttons": "Entrambi i pulsanti", - "button_1": "Primo pulsante", + "both_buttons": "Entrambi", + "button_1": "Primo", "button_2": "Secondo pulsante", "button_3": "Terzo pulsante", "button_4": "Quarto pulsante", diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 0ddff8557ec14c..923a2beb2ffba2 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\ub450 \uac1c", + "button_1": "\uccab \ubc88\uc9f8", + "button_2": "\ub450 \ubc88\uc9f8", + "button_3": "\uc138 \ubc88\uc9f8", + "button_4": "\ub124 \ubc88\uc9f8", + "close": "\ub2eb\uae30", + "dim_down": "\uc5b4\ub461\uac8c \ud558\uae30", + "dim_up": "\ubc1d\uac8c \ud558\uae30", + "left": "\uc67c\ucabd", + "open": "\uc5f4\uae30", + "right": "\uc624\ub978\ucabd", + "turn_off": "\ub044\uae30", + "turn_on": "\ucf1c\uae30" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", + "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", + "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", + "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", + "remote_button_rotated": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", + "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984", + "remote_gyro_activated": "\uae30\uae30 \ud754\ub4e6" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 506461ea50e8e0..994e13f567474c 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -41,6 +41,23 @@ }, "title": "Brama deCONZ Zigbee" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Oba przyciski", + "button_1": "Pierwszy przycisk", + "button_2": "Drugi przycisk", + "button_3": "Trzeci przycisk", + "button_4": "Czwarty przycisk", + "close": "Zamknij", + "dim_down": "Przyciemnienie", + "dim_up": "Przyciemnienie", + "left": "Lewo", + "open": "Otw\u00f3rz", + "right": "Prawo", + "turn_off": "Wy\u0142\u0105cz", + "turn_on": "W\u0142\u0105cz" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 23e98919bb8076..92fd1e3e7490c6 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ", @@ -25,7 +25,7 @@ "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" }, - "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ" + "title": "deCONZ" }, "link": { "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb", @@ -41,6 +41,30 @@ }, "title": "deCONZ" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u041e\u0431\u0435 \u043a\u043d\u043e\u043f\u043a\u0438", + "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", + "left": "\u041d\u0430\u043b\u0435\u0432\u043e", + "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", + "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e" + }, + "trigger_type": { + "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b", + "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430", + "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/fr.json b/homeassistant/components/geonetnz_quakes/.translations/fr.json new file mode 100644 index 00000000000000..74ae5541754ef7 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Emplacement d\u00e9j\u00e0 enregistr\u00e9" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Rayon" + }, + "title": "Remplissez les d\u00e9tails de votre filtre." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/fr.json b/homeassistant/components/iaqualink/.translations/fr.json index cf449ebb358cbc..97971b99e9f7ab 100644 --- a/homeassistant/components/iaqualink/.translations/fr.json +++ b/homeassistant/components/iaqualink/.translations/fr.json @@ -2,6 +2,20 @@ "config": { "abort": { "already_setup": "Vous ne pouvez configurer qu'une seule connexion iAqualink." - } + }, + "error": { + "connection_failure": "Impossible de se connecter \u00e0 iAqualink. V\u00e9rifiez votre nom d'utilisateur et votre mot de passe." + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur / adresse e-mail" + }, + "description": "Veuillez saisir le nom d'utilisateur et le mot de passe de votre compte iAqualink.", + "title": "Se connecter \u00e0 iAqualink" + } + }, + "title": "Jandy iAqualink" } } \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/fr.json b/homeassistant/components/life360/.translations/fr.json index cb4682fc937103..947425e4807f91 100644 --- a/homeassistant/components/life360/.translations/fr.json +++ b/homeassistant/components/life360/.translations/fr.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Informations d'identification invalides", "invalid_username": "Nom d'utilisateur invalide", + "unexpected": "Erreur inattendue lors de la communication avec le serveur Life360", "user_already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" }, "step": { diff --git a/homeassistant/components/solaredge/.translations/fr.json b/homeassistant/components/solaredge/.translations/fr.json new file mode 100644 index 00000000000000..201e3ff49c6105 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "api_key": "La cl\u00e9 API pour ce site", + "name": "Le nom de cette installation", + "site_id": "L'identifiant de site SolarEdge" + }, + "title": "D\u00e9finir les param\u00e8tres de l'API pour cette installation" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/fr.json b/homeassistant/components/traccar/.translations/fr.json new file mode 100644 index 00000000000000..0948a31739fcea --- /dev/null +++ b/homeassistant/components/traccar/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Votre instance de Home Assistant doit \u00eatre accessible depuis Internet pour recevoir les messages de Traccar.", + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "create_entry": { + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans Traccar. \n\n Utilisez l'URL suivante: ` {webhook_url} ` \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails." + }, + "step": { + "user": { + "description": "\u00cates-vous s\u00fbr de vouloir configurer Traccar?", + "title": "Configurer Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/fr.json b/homeassistant/components/twentemilieu/.translations/fr.json new file mode 100644 index 00000000000000..0321a6b73cec35 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adresse d\u00e9j\u00e0 configur\u00e9e." + }, + "error": { + "connection_error": "\u00c9chec de connexion.", + "invalid_address": "Adresse introuvable dans la zone de service de Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Lettre de la maison / suppl\u00e9mentaire", + "house_number": "Num\u00e9ro de maison", + "post_code": "Code postal" + }, + "description": "Configurez Twente Milieu en fournissant des informations sur la collecte des d\u00e9chets sur votre adresse.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index 9e567fcc394a75..8c2526f8a1565a 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -22,5 +22,17 @@ } }, "title": "Contr\u00f4leur UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Temps en secondes depuis la derni\u00e8re vue avant de consid\u00e9rer comme absent", + "track_clients": "Suivre les clients du r\u00e9seau", + "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)", + "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/fr.json b/homeassistant/components/velbus/.translations/fr.json index f930df12861abe..8d93adbf4a92dd 100644 --- a/homeassistant/components/velbus/.translations/fr.json +++ b/homeassistant/components/velbus/.translations/fr.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "port_exists": "Ce port est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "connection_failed": "La connexion velbus a \u00e9chou\u00e9", + "port_exists": "Ce port est d\u00e9j\u00e0 configur\u00e9" + }, "step": { "user": { "data": { - "name": "Le nom pour cette connexion velbus" + "name": "Le nom pour cette connexion velbus", + "port": "Cha\u00eene de connexion" }, "title": "D\u00e9finir le type de connexion velbus" } From 7e7ec498cac6ad34ef6a2a6315bd1c6480cdc8c0 Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Fri, 13 Sep 2019 07:33:14 +0200 Subject: [PATCH 013/296] Fix Typo (#26612) --- homeassistant/components/cert_expiry/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json index 8943643e8b392e..3e2fea2342e260 100644 --- a/homeassistant/components/cert_expiry/strings.json +++ b/homeassistant/components/cert_expiry/strings.json @@ -14,7 +14,7 @@ "error": { "host_port_exists": "This host and port combination is already configured", "resolve_failed": "This host can not be resolved", - "connection_timeout": "Timeout whemn connecting to this host", + "connection_timeout": "Timeout when connecting to this host", "certificate_fetch_failed": "Can not fetch certificate from this host and port combination" }, "abort": { From 2f6d567657cbfa564080679a6336331c77416318 Mon Sep 17 00:00:00 2001 From: Gilad Peleg Date: Fri, 13 Sep 2019 22:09:45 +0300 Subject: [PATCH 014/296] Refactor Bluetooth Tracker to async (#26614) * Convert bluetooth device tracker to async * WIP * WIP * Fix callback * Fix tracked devices * Perform synchornized updates * Add doc * Run in executor * Improve execution * Improve execution * Don't create a redundant task * Optimize see_device to run concurrently * Remove redundant initialization scan --- .../bluetooth_tracker/device_tracker.py | 128 +++++++++++------- 1 file changed, 76 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 8f01036da75911..6a26775b0a8ac7 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -1,6 +1,7 @@ """Tracking for bluetooth devices.""" +import asyncio import logging -from typing import List, Set, Tuple +from typing import List, Set, Tuple, Optional # pylint: disable=import-error import bluetooth @@ -21,10 +22,9 @@ async_load_config, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.util.async_ import run_coroutine_threadsafe -import homeassistant.util.dt as dt_util + _LOGGER = logging.getLogger(__name__) @@ -65,12 +65,15 @@ def discover_devices(device_id: int) -> List[Tuple[str, str]]: return result -def see_device(see, mac: str, device_name: str, rssi=None) -> None: +async def see_device( + hass: HomeAssistantType, async_see, mac: str, device_name: str, rssi=None +) -> None: """Mark a device as seen.""" attributes = {} if rssi is not None: attributes["rssi"] = rssi - see( + + await async_see( mac=f"{BT_PREFIX}{mac}", host_name=device_name, attributes=attributes, @@ -78,90 +81,111 @@ def see_device(see, mac: str, device_name: str, rssi=None) -> None: ) -def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[str]]: +async def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[str]]: """ Load all known devices. We just need the devices so set consider_home and home range to 0 """ yaml_path: str = hass.config.path(YAML_DEVICES) - devices_to_track: Set[str] = set() - devices_to_not_track: Set[str] = set() - - for device in run_coroutine_threadsafe( - async_load_config(yaml_path, hass, 0), hass.loop - ).result(): - # Check if device is a valid bluetooth device - if not is_bluetooth_device(device): - continue - - normalized_mac: str = device.mac[3:] - if device.track: - devices_to_track.add(normalized_mac) - else: - devices_to_not_track.add(normalized_mac) + + devices = await async_load_config(yaml_path, hass, 0) + bluetooth_devices = [device for device in devices if is_bluetooth_device(device)] + + devices_to_track: Set[str] = { + device.mac[3:] for device in bluetooth_devices if device.track + } + devices_to_not_track: Set[str] = { + device.mac[3:] for device in bluetooth_devices if not device.track + } return devices_to_track, devices_to_not_track -def setup_scanner(hass: HomeAssistantType, config: dict, see, discovery_info=None): +def lookup_name(mac: str) -> Optional[str]: + """Lookup a Bluetooth device name.""" + _LOGGER.debug("Scanning %s", mac) + return bluetooth.lookup_name(mac, timeout=5) + + +async def async_setup_scanner( + hass: HomeAssistantType, config: dict, async_see, discovery_info=None +): """Set up the Bluetooth Scanner.""" device_id: int = config.get(CONF_DEVICE_ID) - devices_to_track, devices_to_not_track = get_tracking_devices(hass) + interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + request_rssi = config.get(CONF_REQUEST_RSSI, False) + update_bluetooth_lock = asyncio.Lock() # If track new devices is true discover new devices on startup. track_new: bool = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) - _LOGGER.debug("Tracking new devices = %s", track_new) + _LOGGER.debug("Tracking new devices is set to %s", track_new) + + devices_to_track, devices_to_not_track = await get_tracking_devices(hass) if not devices_to_track and not track_new: _LOGGER.debug("No Bluetooth devices to track and not tracking new devices") - if track_new: - for mac, device_name in discover_devices(device_id): - if mac not in devices_to_track and mac not in devices_to_not_track: - devices_to_track.add(mac) - see_device(see, mac, device_name) - - interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - - request_rssi = config.get(CONF_REQUEST_RSSI, False) if request_rssi: _LOGGER.debug("Detecting RSSI for devices") - def update_bluetooth(_): - """Update Bluetooth and set timer for the next update.""" - update_bluetooth_once() - track_point_in_utc_time(hass, update_bluetooth, dt_util.utcnow() + interval) + async def perform_bluetooth_update(): + """Discover Bluetooth devices and update status.""" + + _LOGGER.debug("Performing Bluetooth devices discovery and update") + tasks = [] - def update_bluetooth_once(): - """Lookup Bluetooth device and update status.""" try: if track_new: - for mac, device_name in discover_devices(device_id): + devices = await hass.async_add_executor_job(discover_devices, device_id) + for mac, device_name in devices: if mac not in devices_to_track and mac not in devices_to_not_track: devices_to_track.add(mac) for mac in devices_to_track: - _LOGGER.debug("Scanning %s", mac) - device_name = bluetooth.lookup_name(mac, timeout=5) + device_name = await hass.async_add_executor_job(lookup_name, mac) + if device_name is None: + # Could not lookup device name + continue + rssi = None if request_rssi: client = BluetoothRSSI(mac) - rssi = client.request_rssi() + rssi = await hass.async_add_executor_job(client.request_rssi) client.close() - if device_name is None: - # Could not lookup device name - continue - see_device(see, mac, device_name, rssi) + + tasks.append(see_device(hass, async_see, mac, device_name, rssi)) + + if tasks: + await asyncio.wait(tasks) + except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") - def handle_update_bluetooth(call): + async def update_bluetooth(now=None): + """Lookup Bluetooth devices and update status.""" + + # If an update is in progress, we don't do anything + if update_bluetooth_lock.locked(): + _LOGGER.debug( + "Previous execution of update_bluetooth is taking longer than the scheduled update of interval %s", + interval, + ) + return + + async with update_bluetooth_lock: + await perform_bluetooth_update() + + async def handle_manual_update_bluetooth(call): """Update bluetooth devices on demand.""" - update_bluetooth_once() - update_bluetooth(dt_util.utcnow()) + await update_bluetooth() + + hass.async_create_task(update_bluetooth()) + async_track_time_interval(hass, update_bluetooth, interval) - hass.services.register(DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth) + hass.services.async_register( + DOMAIN, "bluetooth_tracker_update", handle_manual_update_bluetooth + ) return True From e4bf2c47168889ca6339524fa66bd1a194b4250d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 13 Sep 2019 22:04:02 +0200 Subject: [PATCH 015/296] Update azure-pipelines-wheels.yml --- azure-pipelines-wheels.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 8c534a88d30457..0c614a9dab2561 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -67,7 +67,6 @@ jobs: sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} - sed -i "s|# VL53L1X|VL53L1X|g" ${requirement_file} sed -i "s|# bme680|bme680|g" ${requirement_file} done displayName: 'Prepare requirements files for Hass.io' From 357f2421c80c92f1de857b71d43bfd6a1add1a96 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 13 Sep 2019 22:29:39 +0200 Subject: [PATCH 016/296] Update azure-pipelines-wheels.yml for Azure Pipelines --- azure-pipelines-wheels.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 0c614a9dab2561..42815d8c8ae779 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -68,5 +68,9 @@ jobs: sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} sed -i "s|# bme680|bme680|g" ${requirement_file} + + if [[ "$(buildArch)" =~ arm ]]; then + sed -i "s|# VL53L1X|VL53L1X|g" ${requirement_file} + fi done displayName: 'Prepare requirements files for Hass.io' From fb1acfccc93063d0e479265af69d6564ea192415 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 14 Sep 2019 00:16:37 +0200 Subject: [PATCH 017/296] deCONZ - create deconz_events through sensor platform (#26592) * Move event creation into sensor platform where it belongs * Fixed the weird failing test observed during device automation PR --- homeassistant/components/deconz/gateway.py | 23 +------ homeassistant/components/deconz/sensor.py | 9 +++ tests/components/deconz/test_deconz_event.py | 60 +++++++++++++++++ tests/components/deconz/test_gateway.py | 68 -------------------- 4 files changed, 70 insertions(+), 90 deletions(-) create mode 100644 tests/components/deconz/test_deconz_event.py diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 35cf63fc3d228a..a090dca0d0c632 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -3,7 +3,6 @@ import async_timeout from pydeconz import DeconzSession, errors -from pydeconz.sensor import Switch from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.const import CONF_HOST @@ -29,10 +28,9 @@ DEFAULT_ALLOW_DECONZ_GROUPS, DOMAIN, NEW_DEVICE, - NEW_SENSOR, SUPPORTED_PLATFORMS, ) -from .deconz_event import DeconzEvent + from .errors import AuthenticationRequired, CannotConnect @@ -119,14 +117,6 @@ async def async_setup(self): ) ) - self.listeners.append( - async_dispatcher_connect( - hass, self.async_signal_new_device(NEW_SENSOR), self.async_add_remote - ) - ) - - self.async_add_remote(self.api.sensors.values()) - self.api.start() self.config_entry.add_update_listener(self.async_new_address) @@ -185,17 +175,6 @@ def async_add_device_callback(self, device_type, device): self.hass, self.async_signal_new_device(device_type), device ) - @callback - def async_add_remote(self, sensors): - """Set up remote from deCONZ.""" - for sensor in sensors: - if sensor.type in Switch.ZHATYPE and not ( - not self.option_allow_clip_sensor and sensor.type.startswith("CLIP") - ): - event = DeconzEvent(sensor, self) - self.hass.async_create_task(event.async_update_device_registry()) - self.events.append(event) - @callback def shutdown(self, event): """Wrap the call to deconz.close. diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index d84a47c6aaf8e3..a6138087f1ce40 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -13,6 +13,7 @@ from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice +from .deconz_event import DeconzEvent from .gateway import get_gateway_from_config_entry, DeconzEntityHandler ATTR_CURRENT = "current" @@ -42,6 +43,14 @@ def async_add_sensor(sensors): if not sensor.BINARY: if sensor.type in Switch.ZHATYPE: + + if gateway.option_allow_clip_sensor or not sensor.type.startswith( + "CLIP" + ): + event = DeconzEvent(sensor, gateway) + hass.async_create_task(event.async_update_device_registry()) + gateway.events.append(event) + if sensor.battery: entities.append(DeconzBattery(sensor, gateway)) diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py new file mode 100644 index 00000000000000..72966ba6c66b09 --- /dev/null +++ b/tests/components/deconz/test_deconz_event.py @@ -0,0 +1,60 @@ +"""Test deCONZ remote events.""" +from unittest.mock import Mock + +from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT, DeconzEvent +from homeassistant.core import callback + + +async def test_create_event(hass): + """Successfully created a deCONZ event.""" + mock_remote = Mock() + mock_remote.name = "Name" + + mock_gateway = Mock() + mock_gateway.hass = hass + + event = DeconzEvent(mock_remote, mock_gateway) + + assert event.event_id == "name" + + +async def test_update_event(hass): + """Successfully update a deCONZ event.""" + mock_remote = Mock() + mock_remote.name = "Name" + + mock_gateway = Mock() + mock_gateway.hass = hass + + event = DeconzEvent(mock_remote, mock_gateway) + mock_remote.changed_keys = {"state": True} + + calls = [] + + @callback + def listener(event): + """Mock listener.""" + calls.append(event) + + unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, listener) + + event.async_update_callback() + await hass.async_block_till_done() + + assert len(calls) == 1 + + unsub() + + +async def test_remove_event(hass): + """Successfully update a deCONZ event.""" + mock_remote = Mock() + mock_remote.name = "Name" + + mock_gateway = Mock() + mock_gateway.hass = hass + + event = DeconzEvent(mock_remote, mock_gateway) + event.async_will_remove_from_hass() + + assert event._device is None diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index c17aa0b66390ca..d84706430f465b 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -126,24 +126,6 @@ async def test_add_device(hass): assert len(mock_dispatch_send.mock_calls[0]) == 3 -@pytest.mark.skip(reason="fails for unkown reason, will refactor in a separate PR") -async def test_add_remote(hass): - """Successful add remote.""" - entry = Mock() - entry.data = ENTRY_CONFIG - - remote = Mock() - remote.name = "name" - remote.type = "ZHASwitch" - remote.register_async_callback = Mock() - - deconz_gateway = gateway.DeconzGateway(hass, entry) - deconz_gateway.async_add_remote([remote]) - await hass.async_block_till_done() - - assert len(deconz_gateway.events) == 1 - - async def test_shutdown(): """Successful shutdown.""" hass = Mock() @@ -218,53 +200,3 @@ async def test_get_gateway_fails_cannot_connect(hass): side_effect=pydeconz.errors.RequestError, ), pytest.raises(errors.CannotConnect): assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False - - -@pytest.mark.skip(reason="fails for unkown reason, will refactor in a separate PR") -async def test_create_event(hass): - """Successfully created a deCONZ event.""" - mock_remote = Mock() - mock_remote.name = "Name" - - mock_gateway = Mock() - mock_gateway.hass = hass - - event = gateway.DeconzEvent(mock_remote, mock_gateway) - await hass.async_block_till_done() - - assert event.event_id == "name" - - -@pytest.mark.skip(reason="fails for unkown reason, will refactor in a separate PR") -async def test_update_event(hass): - """Successfully update a deCONZ event.""" - hass.bus.async_fire = Mock() - - mock_remote = Mock() - mock_remote.name = "Name" - - mock_gateway = Mock() - mock_gateway.hass = hass - - event = gateway.DeconzEvent(mock_remote, mock_gateway) - await hass.async_block_till_done() - mock_remote.changed_keys = {"state": True} - event.async_update_callback() - - assert len(hass.bus.async_fire.mock_calls) == 1 - - -@pytest.mark.skip(reason="fails for unkown reason, will refactor in a separate PR") -async def test_remove_event(hass): - """Successfully update a deCONZ event.""" - mock_remote = Mock() - mock_remote.name = "Name" - - mock_gateway = Mock() - mock_gateway.hass = hass - - event = gateway.DeconzEvent(mock_remote, mock_gateway) - await hass.async_block_till_done() - event.async_will_remove_from_hass() - - assert event._device is None From 6a9ecf00154d51dbd530c2d63ed70aa1440e052f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 14 Sep 2019 00:32:15 +0000 Subject: [PATCH 018/296] [ci skip] Translation update --- .../components/adguard/.translations/es.json | 13 +++++++++ .../cert_expiry/.translations/en.json | 2 +- .../cert_expiry/.translations/it.json | 2 +- .../components/deconz/.translations/es.json | 11 +++++++ .../components/deconz/.translations/pl.json | 12 ++++++++ .../components/deconz/.translations/sl.json | 29 +++++++++++++++++++ .../iaqualink/.translations/es.json | 12 ++++++++ .../iaqualink/.translations/sl.json | 21 ++++++++++++++ .../components/life360/.translations/es.json | 3 +- .../components/light/.translations/es.json | 12 ++++++-- .../components/light/.translations/sl.json | 9 ++++++ .../components/linky/.translations/es.json | 15 ++++++++++ .../solaredge/.translations/es.json | 13 +++++++++ .../solaredge/.translations/sl.json | 21 ++++++++++++++ .../components/switch/.translations/es.json | 16 ++++++++++ .../components/switch/.translations/sl.json | 17 +++++++++++ .../components/traccar/.translations/es.json | 12 +++++++- .../twentemilieu/.translations/es.json | 3 ++ .../components/velbus/.translations/es.json | 3 ++ 19 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/iaqualink/.translations/es.json create mode 100644 homeassistant/components/iaqualink/.translations/sl.json create mode 100644 homeassistant/components/linky/.translations/es.json create mode 100644 homeassistant/components/solaredge/.translations/es.json create mode 100644 homeassistant/components/solaredge/.translations/sl.json create mode 100644 homeassistant/components/switch/.translations/es.json create mode 100644 homeassistant/components/switch/.translations/sl.json diff --git a/homeassistant/components/adguard/.translations/es.json b/homeassistant/components/adguard/.translations/es.json index 971d38f9ab2d26..46f21d96195465 100644 --- a/homeassistant/components/adguard/.translations/es.json +++ b/homeassistant/components/adguard/.translations/es.json @@ -2,6 +2,19 @@ "config": { "abort": { "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente." + }, + "error": { + "connection_error": "No se conect\u00f3." + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json index b6aa1cefb02aed..873dfee9a92bb9 100644 --- a/homeassistant/components/cert_expiry/.translations/en.json +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Can not fetch certificate from this host and port combination", - "connection_timeout": "Timeout whemn connecting to this host", + "connection_timeout": "Timeout when connecting to this host", "host_port_exists": "This host and port combination is already configured", "resolve_failed": "This host can not be resolved" }, diff --git a/homeassistant/components/cert_expiry/.translations/it.json b/homeassistant/components/cert_expiry/.translations/it.json index 9135ed3b478592..73749382dd9bca 100644 --- a/homeassistant/components/cert_expiry/.translations/it.json +++ b/homeassistant/components/cert_expiry/.translations/it.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Non \u00e8 possibile recuperare il certificato da questa combinazione di host e porta", - "connection_timeout": "Tempo scaduto durante la connessione a questo host", + "connection_timeout": "Tempo scaduto collegandosi a questo host", "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", "resolve_failed": "Questo host non pu\u00f2 essere risolto" }, diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 8bcf03914cee84..3d2b3f17814a49 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -39,6 +39,17 @@ }, "title": "Pasarela Zigbee deCONZ" }, + "device_automation": { + "trigger_subtype": { + "close": "Cerrar", + "dim_down": "Bajar la intensidad", + "dim_up": "Subir la intensidad", + "left": "Izquierda", + "right": "Derecha", + "turn_off": "Apagar", + "turn_on": "Encender" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 994e13f567474c..70c33cf3c02f4f 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -56,6 +56,18 @@ "right": "Prawo", "turn_off": "Wy\u0142\u0105cz", "turn_on": "W\u0142\u0105cz" + }, + "trigger_type": { + "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "Przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "Przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_rotated": "Przycisk obr\u00f3cony \"{subtype}\"", + "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty", + "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony", + "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", + "remote_gyro_activated": "Urz\u0105dzenie potrz\u0105\u015bni\u0119te" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 86210b2e6c1065..9aebb2a556f6cd 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee prehod" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Oba gumba", + "button_1": "Prvi gumb", + "button_2": "Drugi gumb", + "button_3": "Tretji gumb", + "button_4": "\u010cetrti gumb", + "close": "Zapri", + "dim_down": "Zatemnite", + "dim_up": "pove\u010dajte mo\u010d", + "left": "Levo", + "open": "Odprto", + "right": "Desno", + "turn_off": "Ugasni", + "turn_on": "Pri\u017egi" + }, + "trigger_type": { + "remote_button_double_press": "Dvakrat kliknete gumb \"{subtype}\"", + "remote_button_long_press": "\"{subtype}\" gumb neprekinjeno pritisnjen", + "remote_button_long_release": "\"{subtype}\" gumb spro\u0161\u010den po dolgem pritisku", + "remote_button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen", + "remote_button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen", + "remote_button_rotated": "Gumb \"{subtype}\" zasukan", + "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", + "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", + "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", + "remote_gyro_activated": "Naprava se je pretresla" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/iaqualink/.translations/es.json b/homeassistant/components/iaqualink/.translations/es.json new file mode 100644 index 00000000000000..7326d80497be4a --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/es.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario / correo electr\u00f3nico" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/sl.json b/homeassistant/components/iaqualink/.translations/sl.json new file mode 100644 index 00000000000000..e2a7f94b3d8a70 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Konfigurirate lahko samo eno povezavo iAqualink." + }, + "error": { + "connection_failure": "Ne morete vzpostaviti povezave z iAqualink. Preverite va\u0161e uporabni\u0161ko ime in geslo." + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime / e-po\u0161tni naslov" + }, + "description": "Prosimo, vnesite uporabni\u0161ko ime in geslo za iAqualink ra\u010dun.", + "title": "Pove\u017eite se z iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/es.json b/homeassistant/components/life360/.translations/es.json index 8fc70a60a052e6..28999de5e81b94 100644 --- a/homeassistant/components/life360/.translations/es.json +++ b/homeassistant/components/life360/.translations/es.json @@ -9,7 +9,8 @@ }, "error": { "invalid_credentials": "Credenciales no v\u00e1lidas", - "invalid_username": "Nombre de usuario no v\u00e1lido" + "invalid_username": "Nombre de usuario no v\u00e1lido", + "user_already_configured": "La cuenta ya ha sido configurada" }, "step": { "user": { diff --git a/homeassistant/components/light/.translations/es.json b/homeassistant/components/light/.translations/es.json index b56875453dd67e..93dfc65bbe1c0a 100644 --- a/homeassistant/components/light/.translations/es.json +++ b/homeassistant/components/light/.translations/es.json @@ -1,8 +1,16 @@ { "device_automation": { + "action_type": { + "turn_off": "Apagar {entity_name}", + "turn_on": "Encender {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 apagada", + "is_on": "{entity_name} est\u00e1 encendida" + }, "trigger_type": { - "turn_off": "{nombre} desactivado", - "turn_on": "{nombre} activado" + "turn_off": "{entity_name} apagada", + "turn_on": "{entity_name} encendida" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/sl.json b/homeassistant/components/light/.translations/sl.json index 68e770e88731b8..afd59d619e0cdc 100644 --- a/homeassistant/components/light/.translations/sl.json +++ b/homeassistant/components/light/.translations/sl.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Preklopite {entity_name}", + "turn_off": "Izklopite {entity_name}", + "turn_on": "Vklopite {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen" + }, "trigger_type": { "turn_off": "{name} izklopljeno", "turn_on": "{name} vklopljeno" diff --git a/homeassistant/components/linky/.translations/es.json b/homeassistant/components/linky/.translations/es.json new file mode 100644 index 00000000000000..7c0d17c8a8f1e6 --- /dev/null +++ b/homeassistant/components/linky/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "username_exists": "Cuenta ya configurada" + }, + "error": { + "wrong_login": "Error de inicio de sesi\u00f3n: compruebe su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a" + }, + "step": { + "user": { + "description": "Introduzca sus credenciales" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/es.json b/homeassistant/components/solaredge/.translations/es.json new file mode 100644 index 00000000000000..9f52511a165cff --- /dev/null +++ b/homeassistant/components/solaredge/.translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "La clave de la API para este sitio", + "name": "El nombre de esta instalaci\u00f3n" + }, + "title": "Definir los par\u00e1metros de la API para esta instalaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/sl.json b/homeassistant/components/solaredge/.translations/sl.json new file mode 100644 index 00000000000000..ebfefe40b0e54d --- /dev/null +++ b/homeassistant/components/solaredge/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Ta site_id je \u017ee nastavljen" + }, + "error": { + "site_exists": "Ta site_id je \u017ee nastavljen" + }, + "step": { + "user": { + "data": { + "api_key": "API klju\u010d za to stran", + "name": "Ime te namestitve", + "site_id": "SolarEdge site-ID" + }, + "title": "Dolo\u010dite parametre API za to namestitev" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/es.json b/homeassistant/components/switch/.translations/es.json new file mode 100644 index 00000000000000..6749eab129331a --- /dev/null +++ b/homeassistant/components/switch/.translations/es.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Apagar {entity_name}", + "turn_on": "Encender {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} apagado", + "turn_on": "{entity_name} encendido" + }, + "trigger_type": { + "turn_off": "{entity_name} apagado", + "turn_on": "{entity_name} encendido" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/sl.json b/homeassistant/components/switch/.translations/sl.json new file mode 100644 index 00000000000000..38edfe5a1953d7 --- /dev/null +++ b/homeassistant/components/switch/.translations/sl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Preklopite {entity_name}", + "turn_off": "Izklopite {entity_name}", + "turn_on": "Vklopite {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} izklopljen", + "turn_on": "{entity_name} vklopljen" + }, + "trigger_type": { + "turn_off": "{entity_name} izklopljen", + "turn_on": "{entity_name} vklopljen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/es.json b/homeassistant/components/traccar/.translations/es.json index ab8c0e70cd42e4..b0b65a10c83cc9 100644 --- a/homeassistant/components/traccar/.translations/es.json +++ b/homeassistant/components/traccar/.translations/es.json @@ -1,7 +1,17 @@ { "config": { "abort": { - "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Traccar." + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Traccar.", + "one_instance_allowed": "S\u00f3lo se necesita una \u00fanica instancia." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, necesitar\u00e1 configurar la funci\u00f3n de webhook en Traccar.\n\nUtilice la siguiente url: ``{webhook_url}``\n\nConsulte la [documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1 seguro de querer configurar Traccar?", + "title": "Configurar Traccar" + } } } } \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/es.json b/homeassistant/components/twentemilieu/.translations/es.json index 02dcb71f54e5ff..902e28b2080d87 100644 --- a/homeassistant/components/twentemilieu/.translations/es.json +++ b/homeassistant/components/twentemilieu/.translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "address_exists": "Direcci\u00f3n ya configurada." + }, "error": { "connection_error": "No se conect\u00f3." }, diff --git a/homeassistant/components/velbus/.translations/es.json b/homeassistant/components/velbus/.translations/es.json index e60ef7b4c676f1..1acaaa53ab2f11 100644 --- a/homeassistant/components/velbus/.translations/es.json +++ b/homeassistant/components/velbus/.translations/es.json @@ -3,6 +3,9 @@ "abort": { "port_exists": "Este puerto ya est\u00e1 configurado" }, + "error": { + "port_exists": "Este puerto ya est\u00e1 configurado" + }, "step": { "user": { "data": { From bca7363a80f5f0bf664a4196333e973c9dd6c348 Mon Sep 17 00:00:00 2001 From: Dan Ponte Date: Fri, 13 Sep 2019 22:06:09 -0400 Subject: [PATCH 019/296] zha ZCL color loop effect (#26549) * Initial implementation of ZCL color loop effect * Fix linter complaints * Use const for action * Reformat with Black * Cleanup after review. * Handle effect being None --- homeassistant/components/zha/light.py | 64 +++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 379f69febbb82f..27257e5039aee2 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -27,9 +27,15 @@ DEFAULT_DURATION = 5 +CAPABILITIES_COLOR_LOOP = 0x4 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 +UPDATE_COLORLOOP_ACTION = 0x1 +UPDATE_COLORLOOP_DIRECTION = 0x2 +UPDATE_COLORLOOP_TIME = 0x4 +UPDATE_COLORLOOP_HUE = 0x8 + UNSUPPORTED_ATTRIBUTE = 0x86 SCAN_INTERVAL = timedelta(minutes=60) PARALLEL_UPDATES = 5 @@ -85,6 +91,8 @@ def __init__(self, unique_id, zha_device, channels, **kwargs): self._color_temp = None self._hs_color = None self._brightness = None + self._effect_list = [] + self._effect = None self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL) self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) @@ -103,6 +111,10 @@ def __init__(self, unique_id, zha_device, channels, **kwargs): self._supported_features |= light.SUPPORT_COLOR self._hs_color = (0, 0) + if color_capabilities & CAPABILITIES_COLOR_LOOP: + self._supported_features |= light.SUPPORT_EFFECT + self._effect_list.append(light.EFFECT_COLORLOOP) + @property def is_on(self) -> bool: """Return true if entity is on.""" @@ -141,6 +153,16 @@ def color_temp(self): """Return the CT color value in mireds.""" return self._color_temp + @property + def effect_list(self): + """Return the list of supported effects.""" + return self._effect_list + + @property + def effect(self): + """Return the current effect.""" + return self._effect + @property def supported_features(self): """Flag supported features.""" @@ -173,12 +195,15 @@ def async_restore_last_state(self, last_state): self._color_temp = last_state.attributes["color_temp"] if "hs_color" in last_state.attributes: self._hs_color = last_state.attributes["hs_color"] + if "effect" in last_state.attributes: + self._effect = last_state.attributes["effect"] async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) duration = transition * 10 if transition else DEFAULT_DURATION brightness = kwargs.get(light.ATTR_BRIGHTNESS) + effect = kwargs.get(light.ATTR_EFFECT) t_log = {} if ( @@ -234,6 +259,36 @@ async def async_turn_on(self, **kwargs): return self._hs_color = hs_color + if ( + effect == light.EFFECT_COLORLOOP + and self.supported_features & light.SUPPORT_EFFECT + ): + result = await self._color_channel.color_loop_set( + UPDATE_COLORLOOP_ACTION + | UPDATE_COLORLOOP_DIRECTION + | UPDATE_COLORLOOP_TIME, + 0x2, # start from current hue + 0x1, # only support up + transition if transition else 7, # transition + 0, # no hue + ) + t_log["color_loop_set"] = result + self._effect = light.EFFECT_COLORLOOP + elif ( + self._effect == light.EFFECT_COLORLOOP + and effect != light.EFFECT_COLORLOOP + and self.supported_features & light.SUPPORT_EFFECT + ): + result = await self._color_channel.color_loop_set( + UPDATE_COLORLOOP_ACTION, + 0x0, + 0x0, + 0x0, + 0x0, # update action only, action off, no dir,time,hue + ) + t_log["color_loop_set"] = result + self._effect = None + self.debug("turned on: %s", t_log) self.async_schedule_update_ha_state() @@ -292,6 +347,15 @@ async def async_get_state(self, from_cache=True): self._hs_color = color_util.color_xy_to_hs( float(color_x / 65535), float(color_y / 65535) ) + if ( + color_capabilities is not None + and color_capabilities & CAPABILITIES_COLOR_LOOP + ): + color_loop_active = await self._color_channel.get_attribute_value( + "color_loop_active", from_cache=from_cache + ) + if color_loop_active is not None and color_loop_active == 1: + self._effect = light.EFFECT_COLORLOOP async def refresh(self, time): """Call async_get_state at an interval.""" From a71cd6e90e54c2411dc04a689a7c7a3f170899ee Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Fri, 13 Sep 2019 22:05:47 -0700 Subject: [PATCH 020/296] Add iaqualink binary sensor and unique_id (#26616) * Add binary_platform to iaqualink integration, add unique_id * Revert Mixin changes, move self.dev to AqualinkEntity * Style fixes --- .coveragerc | 1 + .../components/iaqualink/__init__.py | 20 ++++++++ .../components/iaqualink/binary_sensor.py | 48 +++++++++++++++++++ homeassistant/components/iaqualink/climate.py | 14 +----- homeassistant/components/iaqualink/light.py | 8 +--- homeassistant/components/iaqualink/sensor.py | 6 --- homeassistant/components/iaqualink/switch.py | 8 +--- 7 files changed, 74 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/iaqualink/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 0c6ac82894a6f4..fef77c8a247bba 100644 --- a/.coveragerc +++ b/.coveragerc @@ -289,6 +289,7 @@ omit = homeassistant/components/hydrawise/* homeassistant/components/hyperion/light.py homeassistant/components/ialarm/alarm_control_panel.py + homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/climate.py homeassistant/components/iaqualink/light.py homeassistant/components/iaqualink/sensor.py diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 56a39df64c9dea..dec91186be2869 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -7,7 +7,9 @@ import voluptuous as vol from iaqualink import ( + AqualinkBinarySensor, AqualinkClient, + AqualinkDevice, AqualinkLight, AqualinkLoginException, AqualinkSensor, @@ -16,6 +18,7 @@ ) from homeassistant import config_entries +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -76,6 +79,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None password = entry.data[CONF_PASSWORD] # These will contain the initialized devices + binary_sensors = hass.data[DOMAIN][BINARY_SENSOR_DOMAIN] = [] climates = hass.data[DOMAIN][CLIMATE_DOMAIN] = [] lights = hass.data[DOMAIN][LIGHT_DOMAIN] = [] sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] @@ -103,12 +107,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None climates += [dev] elif isinstance(dev, AqualinkLight): lights += [dev] + elif isinstance(dev, AqualinkBinarySensor): + binary_sensors += [dev] elif isinstance(dev, AqualinkSensor): sensors += [dev] elif isinstance(dev, AqualinkToggle): switches += [dev] forward_setup = hass.config_entries.async_forward_entry_setup + if binary_sensors: + _LOGGER.debug("Got %s binary sensors: %s", len(binary_sensors), binary_sensors) + hass.async_create_task(forward_setup(entry, BINARY_SENSOR_DOMAIN)) if climates: _LOGGER.debug("Got %s climates: %s", len(climates), climates) hass.async_create_task(forward_setup(entry, CLIMATE_DOMAIN)) @@ -138,6 +147,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo tasks = [] + if hass.data[DOMAIN][BINARY_SENSOR_DOMAIN]: + tasks += [forward_unload(entry, BINARY_SENSOR_DOMAIN)] if hass.data[DOMAIN][CLIMATE_DOMAIN]: tasks += [forward_unload(entry, CLIMATE_DOMAIN)] if hass.data[DOMAIN][LIGHT_DOMAIN]: @@ -174,6 +185,10 @@ class AqualinkEntity(Entity): class. """ + def __init__(self, dev: AqualinkDevice): + """Initialize the entity.""" + self.dev = dev + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._update_callback) @@ -190,3 +205,8 @@ def should_poll(self) -> bool: updates on a timer. """ return False + + @property + def unique_id(self) -> str: + """Return a unique identifier for this entity.""" + return f"{self.dev.system.serial}_{self.dev.name}" diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py new file mode 100644 index 00000000000000..09c9322a58764b --- /dev/null +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -0,0 +1,48 @@ +"""Support for Aqualink temperature sensors.""" +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + DEVICE_CLASS_COLD, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import AqualinkEntity +from .const import DOMAIN as AQUALINK_DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up discovered binary sensors.""" + devs = [] + for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: + devs.append(HassAqualinkBinarySensor(dev)) + async_add_entities(devs, True) + + +class HassAqualinkBinarySensor(AqualinkEntity, BinarySensorDevice): + """Representation of a binary sensor.""" + + @property + def name(self) -> str: + """Return the name of the binary sensor.""" + return self.dev.label + + @property + def is_on(self) -> bool: + """Return whether the binary sensor is on or not.""" + return self.dev.is_on + + @property + def device_class(self) -> str: + """Return the class of the binary sensor.""" + if self.name == "Freeze Protection": + return DEVICE_CLASS_COLD + return None diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 321c54329a2878..f41d17837c2f6c 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -2,13 +2,7 @@ import logging from typing import List, Optional -from iaqualink import ( - AqualinkState, - AqualinkHeater, - AqualinkPump, - AqualinkSensor, - AqualinkThermostat, -) +from iaqualink import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState from iaqualink.const import ( AQUALINK_TEMP_CELSIUS_HIGH, AQUALINK_TEMP_CELSIUS_LOW, @@ -45,13 +39,9 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkThermostat(ClimateDevice, AqualinkEntity): +class HassAqualinkThermostat(AqualinkEntity, ClimateDevice): """Representation of a thermostat.""" - def __init__(self, dev: AqualinkThermostat): - """Initialize the thermostat.""" - self.dev = dev - @property def name(self) -> str: """Return the name of the thermostat.""" diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index fbfb10783ee4a1..813af7863f1700 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -1,7 +1,7 @@ """Support for Aqualink pool lights.""" import logging -from iaqualink import AqualinkLight, AqualinkLightEffect +from iaqualink import AqualinkLightEffect from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -32,13 +32,9 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkLight(Light, AqualinkEntity): +class HassAqualinkLight(AqualinkEntity, Light): """Representation of a light.""" - def __init__(self, dev: AqualinkLight): - """Initialize the light.""" - self.dev = dev - @property def name(self) -> str: """Return the name of the light.""" diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index 4a1691e0314caa..81021d0b4471d4 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -2,8 +2,6 @@ import logging from typing import Optional -from iaqualink import AqualinkSensor - from homeassistant.components.sensor import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -30,10 +28,6 @@ async def async_setup_entry( class HassAqualinkSensor(AqualinkEntity): """Representation of a sensor.""" - def __init__(self, dev: AqualinkSensor): - """Initialize the sensor.""" - self.dev = dev - @property def name(self) -> str: """Return the name of the sensor.""" diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py index f2fc51ce713136..8efb473cf54d35 100644 --- a/homeassistant/components/iaqualink/switch.py +++ b/homeassistant/components/iaqualink/switch.py @@ -1,8 +1,6 @@ """Support for Aqualink pool feature switches.""" import logging -from iaqualink import AqualinkToggle - from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -25,13 +23,9 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkSwitch(SwitchDevice, AqualinkEntity): +class HassAqualinkSwitch(AqualinkEntity, SwitchDevice): """Representation of a switch.""" - def __init__(self, dev: AqualinkToggle): - """Initialize the switch.""" - self.dev = dev - @property def name(self) -> str: """Return the name of the switch.""" From 1d3f2d20d2eb2d12d02cde93d7d79e66f8157a1c Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 14 Sep 2019 07:12:19 +0200 Subject: [PATCH 021/296] Add group attribute to Homematic IP Cloud (#26618) * Add group attribute to Homematic IP Cloud * Fix docstring --- homeassistant/components/homematicip_cloud/binary_sensor.py | 4 ++-- homeassistant/components/homematicip_cloud/device.py | 3 +++ homeassistant/components/homematicip_cloud/sensor.py | 6 +++--- homeassistant/components/homematicip_cloud/switch.py | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 594f4f6c54aa4e..4ac4614379b905 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -38,7 +38,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_MODEL_TYPE +from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP, ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) @@ -312,7 +312,7 @@ def available(self) -> bool: @property def device_state_attributes(self): """Return the state attributes of the security zone group.""" - state_attr = {ATTR_MODEL_TYPE: self._device.modelType} + state_attr = {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: True} for attr, attr_key in GROUP_ATTRIBUTES.items(): attr_value = getattr(self._device, attr, None) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 5eeb14b635946c..05853d4b260bca 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -12,6 +12,7 @@ ATTR_MODEL_TYPE = "model_type" ATTR_ID = "id" +ATTR_IS_GROUP = "is_group" # RSSI HAP -> Device ATTR_RSSI_DEVICE = "rssi_device" # RSSI Device -> HAP @@ -131,4 +132,6 @@ def device_state_attributes(self): if attr_value: state_attr[attr_key] = attr_value + state_attr[ATTR_IS_GROUP] = False + return state_attr diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 43812df94d2283..770921288b9341 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -35,7 +35,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_MODEL_TYPE +from .device import ATTR_IS_GROUP, ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) @@ -150,8 +150,8 @@ def unit_of_measurement(self) -> str: @property def device_state_attributes(self): - """Return the state attributes of the security zone group.""" - return {ATTR_MODEL_TYPE: self._device.modelType} + """Return the state attributes of the access point.""" + return {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: False} class HomematicipHeatingThermostat(HomematicipGenericDevice): diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 058e21262e3e88..ababf793f0ce6e 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE +from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP _LOGGER = logging.getLogger(__name__) @@ -113,7 +113,7 @@ def available(self) -> bool: @property def device_state_attributes(self): """Return the state attributes of the switch-group.""" - state_attr = {} + state_attr = {ATTR_IS_GROUP: True} if self._device.unreach: state_attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True return state_attr From 5885c3f3535f8124df949c957e7406ddcdf43e23 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 14 Sep 2019 15:15:06 +0200 Subject: [PATCH 022/296] Move deCONZ services to their own file (#26645) --- homeassistant/components/deconz/__init__.py | 123 +-------------- homeassistant/components/deconz/services.py | 157 ++++++++++++++++++++ 2 files changed, 162 insertions(+), 118 deletions(-) create mode 100644 homeassistant/components/deconz/services.py diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 56663c6b2dab0c..af9f619cb3e1bd 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -10,10 +10,10 @@ ) from homeassistant.helpers import config_validation as cv -# Loading the config flow file will register the flow from .config_flow import get_master_gateway -from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DEFAULT_PORT, DOMAIN, _LOGGER +from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DEFAULT_PORT, DOMAIN from .gateway import DeconzGateway +from .services import async_setup_services, async_unload_services CONFIG_SCHEMA = vol.Schema( { @@ -28,28 +28,6 @@ extra=vol.ALLOW_EXTRA, ) -SERVICE_DECONZ = "configure" - -SERVICE_FIELD = "field" -SERVICE_ENTITY = "entity" -SERVICE_DATA = "data" - -SERVICE_SCHEMA = vol.All( - vol.Schema( - { - vol.Optional(SERVICE_ENTITY): cv.entity_id, - vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"), - vol.Required(SERVICE_DATA): dict, - vol.Optional(CONF_BRIDGEID): str, - } - ), - cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD), -) - -SERVICE_DEVICE_REFRESH = "device_refresh" - -SERVICE_DEVICE_REFRESCH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str})) - async def async_setup(hass, config): """Load configuration for deCONZ component. @@ -89,100 +67,10 @@ async def async_setup_entry(hass, config_entry): await gateway.async_update_device_registry() - async def async_configure(call): - """Set attribute of device in deCONZ. - - Entity is used to resolve to a device path (e.g. '/lights/1'). - Field is a string representing either a full path - (e.g. '/lights/1/state') when entity is not specified, or a - subpath (e.g. '/state') when used together with entity. - Data is a json object with what data you want to alter - e.g. data={'on': true}. - { - "field": "/lights/1/state", - "data": {"on": true} - } - See Dresden Elektroniks REST API documentation for details: - http://dresden-elektronik.github.io/deconz-rest-doc/rest/ - """ - field = call.data.get(SERVICE_FIELD, "") - entity_id = call.data.get(SERVICE_ENTITY) - data = call.data[SERVICE_DATA] - - gateway = get_master_gateway(hass) - if CONF_BRIDGEID in call.data: - gateway = hass.data[DOMAIN][call.data[CONF_BRIDGEID]] - - if entity_id: - try: - field = gateway.deconz_ids[entity_id] + field - except KeyError: - _LOGGER.error("Could not find the entity %s", entity_id) - return - - await gateway.api.async_put_state(field, data) - - hass.services.async_register( - DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA - ) - - async def async_refresh_devices(call): - """Refresh available devices from deCONZ.""" - gateway = get_master_gateway(hass) - if CONF_BRIDGEID in call.data: - gateway = hass.data[DOMAIN][call.data[CONF_BRIDGEID]] - - groups = set(gateway.api.groups.keys()) - lights = set(gateway.api.lights.keys()) - scenes = set(gateway.api.scenes.keys()) - sensors = set(gateway.api.sensors.keys()) - - await gateway.api.async_load_parameters() - - gateway.async_add_device_callback( - "group", - [ - group - for group_id, group in gateway.api.groups.items() - if group_id not in groups - ], - ) - - gateway.async_add_device_callback( - "light", - [ - light - for light_id, light in gateway.api.lights.items() - if light_id not in lights - ], - ) - - gateway.async_add_device_callback( - "scene", - [ - scene - for scene_id, scene in gateway.api.scenes.items() - if scene_id not in scenes - ], - ) - - gateway.async_add_device_callback( - "sensor", - [ - sensor - for sensor_id, sensor in gateway.api.sensors.items() - if sensor_id not in sensors - ], - ) - - hass.services.async_register( - DOMAIN, - SERVICE_DEVICE_REFRESH, - async_refresh_devices, - schema=SERVICE_DEVICE_REFRESCH_SCHEMA, - ) + await async_setup_services(hass) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) + return True @@ -191,8 +79,7 @@ async def async_unload_entry(hass, config_entry): gateway = hass.data[DOMAIN].pop(config_entry.data[CONF_BRIDGEID]) if not hass.data[DOMAIN]: - hass.services.async_remove(DOMAIN, SERVICE_DECONZ) - hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) + await async_unload_services(hass) elif gateway.master: await async_update_master_gateway(hass, config_entry) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py new file mode 100644 index 00000000000000..31ba0ff3581260 --- /dev/null +++ b/homeassistant/components/deconz/services.py @@ -0,0 +1,157 @@ +"""deCONZ services.""" +import voluptuous as vol + +from homeassistant.helpers import config_validation as cv + +from .config_flow import get_master_gateway +from .const import CONF_BRIDGEID, DOMAIN, _LOGGER + +DECONZ_SERVICES = "deconz_services" + +SERVICE_FIELD = "field" +SERVICE_ENTITY = "entity" +SERVICE_DATA = "data" + +SERVICE_CONFIGURE_DEVICE = "configure" +SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(SERVICE_ENTITY): cv.entity_id, + vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"), + vol.Required(SERVICE_DATA): dict, + vol.Optional(CONF_BRIDGEID): str, + } + ), + cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD), +) + +SERVICE_DEVICE_REFRESH = "device_refresh" +SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str})) + + +async def async_setup_services(hass): + """Set up services for deCONZ integration.""" + if hass.data.get(DECONZ_SERVICES, False): + return + + hass.data[DECONZ_SERVICES] = True + + async def async_call_deconz_service(service_call): + """Call correct deCONZ service.""" + service = service_call.service + service_data = service_call.data + + if service == SERVICE_CONFIGURE_DEVICE: + await async_configure_service(hass, service_data) + + elif service == SERVICE_DEVICE_REFRESH: + await async_refresh_devices_service(hass, service_data) + + hass.services.async_register( + DOMAIN, + SERVICE_CONFIGURE_DEVICE, + async_call_deconz_service, + schema=SERVICE_CONFIGURE_DEVICE_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_DEVICE_REFRESH, + async_call_deconz_service, + schema=SERVICE_DEVICE_REFRESH_SCHEMA, + ) + + +async def async_unload_services(hass): + """Unload deCONZ services.""" + if not hass.data.get(DECONZ_SERVICES): + return + + hass.data[DECONZ_SERVICES] = False + + hass.services.async_remove(DOMAIN, SERVICE_CONFIGURE_DEVICE) + hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) + + +async def async_configure_service(hass, data): + """Set attribute of device in deCONZ. + + Entity is used to resolve to a device path (e.g. '/lights/1'). + Field is a string representing either a full path + (e.g. '/lights/1/state') when entity is not specified, or a + subpath (e.g. '/state') when used together with entity. + Data is a json object with what data you want to alter + e.g. data={'on': true}. + { + "field": "/lights/1/state", + "data": {"on": true} + } + See Dresden Elektroniks REST API documentation for details: + http://dresden-elektronik.github.io/deconz-rest-doc/rest/ + """ + field = data.get(SERVICE_FIELD, "") + entity_id = data.get(SERVICE_ENTITY) + data = data[SERVICE_DATA] + + gateway = get_master_gateway(hass) + if CONF_BRIDGEID in data: + gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]] + + if entity_id: + try: + field = gateway.deconz_ids[entity_id] + field + except KeyError: + _LOGGER.error("Could not find the entity %s", entity_id) + return + + await gateway.api.async_put_state(field, data) + + +async def async_refresh_devices_service(hass, data): + """Refresh available devices from deCONZ.""" + gateway = get_master_gateway(hass) + if CONF_BRIDGEID in data: + gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]] + + groups = set(gateway.api.groups.keys()) + lights = set(gateway.api.lights.keys()) + scenes = set(gateway.api.scenes.keys()) + sensors = set(gateway.api.sensors.keys()) + + await gateway.api.async_load_parameters() + + gateway.async_add_device_callback( + "group", + [ + group + for group_id, group in gateway.api.groups.items() + if group_id not in groups + ], + ) + + gateway.async_add_device_callback( + "light", + [ + light + for light_id, light in gateway.api.lights.items() + if light_id not in lights + ], + ) + + gateway.async_add_device_callback( + "scene", + [ + scene + for scene_id, scene in gateway.api.scenes.items() + if scene_id not in scenes + ], + ) + + gateway.async_add_device_callback( + "sensor", + [ + sensor + for sensor_id, sensor in gateway.api.sensors.items() + if sensor_id not in sensors + ], + ) From 24f1ff0aefef3695bf5318a69c1a08820d00dce0 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 14 Sep 2019 17:23:23 +0200 Subject: [PATCH 023/296] Add built in weather to Homematic IP Cloud (#26642) --- .../components/homematicip_cloud/weather.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 2d0a69d7d06320..ed9098559a344c 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -7,6 +7,7 @@ AsyncWeatherSensorPro, ) from homematicip.aio.home import AsyncHome +from homematicip.base.enums import WeatherCondition from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry @@ -17,6 +18,24 @@ _LOGGER = logging.getLogger(__name__) +HOME_WEATHER_CONDITION = { + WeatherCondition.CLEAR: "sunny", + WeatherCondition.LIGHT_CLOUDY: "partlycloudy", + WeatherCondition.CLOUDY: "cloudy", + WeatherCondition.CLOUDY_WITH_RAIN: "rainy", + WeatherCondition.CLOUDY_WITH_SNOW_RAIN: "snowy-rainy", + WeatherCondition.HEAVILY_CLOUDY: "cloudy", + WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN: "rainy", + WeatherCondition.HEAVILY_CLOUDY_WITH_STRONG_RAIN: "snowy-rainy", + WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW: "snowy", + WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW_RAIN: "snowy-rainy", + WeatherCondition.HEAVILY_CLOUDY_WITH_THUNDER: "lightning", + WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN_AND_THUNDER: "lightning-rainy", + WeatherCondition.FOGGY: "fog", + WeatherCondition.STRONG_WIND: "windy", + WeatherCondition.UNKNOWN: "", +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud weather sensor.""" @@ -35,6 +54,8 @@ async def async_setup_entry( elif isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus)): devices.append(HomematicipWeatherSensor(home, device)) + devices.append(HomematicipHomeWeather(home)) + if devices: async_add_entities(devices) @@ -95,3 +116,57 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor): def wind_bearing(self) -> float: """Return the wind bearing.""" return self._device.windDirection + + +class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity): + """representation of a HomematicIP Cloud home weather.""" + + def __init__(self, home: AsyncHome) -> None: + """Initialize the home weather.""" + home.weather.modelType = "HmIP-Home-Weather" + super().__init__(home, home) + + @property + def available(self) -> bool: + """Device available.""" + return self._home.connected + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return f"Weather {self._home.location.city}" + + @property + def temperature(self) -> float: + """Return the platform temperature.""" + return self._device.weather.temperature + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def humidity(self) -> int: + """Return the humidity.""" + return self._device.weather.humidity + + @property + def wind_speed(self) -> float: + """Return the wind speed.""" + return round(self._device.weather.windSpeed, 1) + + @property + def wind_bearing(self) -> float: + """Return the wind bearing.""" + return self._device.weather.windDirection + + @property + def attribution(self) -> str: + """Return the attribution.""" + return "Powered by Homematic IP" + + @property + def condition(self) -> str: + """Return the current condition.""" + return HOME_WEATHER_CONDITION.get(self._device.weather.weatherCondition) From 41c9ed5d5133fe19515959b890f8c748bff0bfb8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 14 Sep 2019 19:15:18 +0200 Subject: [PATCH 024/296] deCONZ - battery sensor instead of battery attribute (#26591) * Allow all sensors to create battery sensors * Neither binary sensor, climate nor sensor will have battery attributes --- .../components/deconz/binary_sensor.py | 7 +- homeassistant/components/deconz/climate.py | 6 +- .../components/deconz/deconz_device.py | 4 +- .../components/deconz/deconz_event.py | 5 ++ homeassistant/components/deconz/sensor.py | 75 ++++++++++--------- 5 files changed, 49 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 492b16a603a5bb..b81ecdc5164a75 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -2,7 +2,7 @@ from pydeconz.sensor import Presence, Vibration from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -17,7 +17,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): @@ -56,7 +55,7 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): def async_update_callback(self, force_update=False): """Update the sensor's state.""" changed = set(self._device.changed_keys) - keys = {"battery", "on", "reachable", "state"} + keys = {"on", "reachable", "state"} if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() @@ -79,8 +78,6 @@ def icon(self): def device_state_attributes(self): """Return the state attributes of the sensor.""" attr = {} - if self._device.battery: - attr[ATTR_BATTERY_LEVEL] = self._device.battery if self._device.on is not None: attr[ATTR_ON] = self._device.on diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 1844cb2c97c73f..b7a1ebce22ad48 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -8,7 +8,7 @@ HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -21,7 +21,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): @@ -120,9 +119,6 @@ def device_state_attributes(self): """Return the state attributes of the thermostat.""" attr = {} - if self._device.battery: - attr[ATTR_BATTERY_LEVEL] = self._device.battery - if self._device.offset: attr[ATTR_OFFSET] = self._device.offset diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index e6249b2304cfee..68daee6cf260ca 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -24,10 +24,10 @@ def unique_id(self): @property def serial(self): """Return a serial number for this device.""" - if self.unique_id is None or self.unique_id.count(":") != 7: + if self._device.uniqueid is None or self._device.uniqueid.count(":") != 7: return None - return self.unique_id.split("-", 1)[0] + return self._device.uniqueid.split("-", 1)[0] @property def device_info(self): diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index f6c2d471bbf0a1..31588db1f23833 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -27,6 +27,11 @@ def __init__(self, device, gateway): self.event_id = slugify(self._device.name) _LOGGER.debug("deCONZ event created: %s", self.event_id) + @property + def device(self): + """Return Event device.""" + return self._device + @callback def async_will_remove_from_hass(self) -> None: """Disconnect event object when removed.""" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index a6138087f1ce40..001721d4f00035 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,15 +1,9 @@ """Support for deCONZ sensors.""" from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, - ATTR_TEMPERATURE, - ATTR_VOLTAGE, - DEVICE_CLASS_BATTERY, -) +from homeassistant.const import ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.util import slugify from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice @@ -24,40 +18,47 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ sensors.""" gateway = get_gateway_from_config_entry(hass, config_entry) + batteries = set() entity_handler = DeconzEntityHandler(gateway) @callback def async_add_sensor(sensors): - """Add sensors from deCONZ.""" + """Add sensors from deCONZ. + + Create DeconzEvent if part of ZHAType list. + Create DeconzSensor if not a ZHAType and not a binary sensor. + Create DeconzBattery if sensor has a battery attribute. + """ entities = [] for sensor in sensors: - if not sensor.BINARY: + if sensor.type in Switch.ZHATYPE: - if sensor.type in Switch.ZHATYPE: + if gateway.option_allow_clip_sensor or not sensor.type.startswith( + "CLIP" + ): + new_event = DeconzEvent(sensor, gateway) + hass.async_create_task(new_event.async_update_device_registry()) + gateway.events.append(new_event) - if gateway.option_allow_clip_sensor or not sensor.type.startswith( - "CLIP" - ): - event = DeconzEvent(sensor, gateway) - hass.async_create_task(event.async_update_device_registry()) - gateway.events.append(event) + elif not sensor.BINARY: - if sensor.battery: - entities.append(DeconzBattery(sensor, gateway)) + new_sensor = DeconzSensor(sensor, gateway) + entity_handler.add_entity(new_sensor) + entities.append(new_sensor) - else: - new_sensor = DeconzSensor(sensor, gateway) - entity_handler.add_entity(new_sensor) - entities.append(new_sensor) + if sensor.battery: + new_battery = DeconzBattery(sensor, gateway) + if new_battery.unique_id not in batteries: + batteries.add(new_battery.unique_id) + entities.append(new_battery) async_add_entities(entities, True) @@ -77,7 +78,7 @@ class DeconzSensor(DeconzDevice): def async_update_callback(self, force_update=False): """Update the sensor's state.""" changed = set(self._device.changed_keys) - keys = {"battery", "on", "reachable", "state"} + keys = {"on", "reachable", "state"} if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() @@ -105,8 +106,6 @@ def unit_of_measurement(self): def device_state_attributes(self): """Return the state attributes of the sensor.""" attr = {} - if self._device.battery: - attr[ATTR_BATTERY_LEVEL] = self._device.battery if self._device.on is not None: attr[ATTR_ON] = self._device.on @@ -133,13 +132,6 @@ def device_state_attributes(self): class DeconzBattery(DeconzDevice): """Battery class for when a device is only represented as an event.""" - def __init__(self, device, gateway): - """Register dispatcher callback for update of battery state.""" - super().__init__(device, gateway) - - self._name = "{} {}".format(self._device.name, "Battery Level") - self._unit_of_measurement = "%" - @callback def async_update_callback(self, force_update=False): """Update the battery's state, if needed.""" @@ -148,6 +140,11 @@ def async_update_callback(self, force_update=False): if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return f"{self.serial}-battery" + @property def state(self): """Return the state of the battery.""" @@ -156,7 +153,7 @@ def state(self): @property def name(self): """Return the name of the battery.""" - return self._name + return f"{self._device.name} Battery Level" @property def device_class(self): @@ -166,10 +163,16 @@ def device_class(self): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return self._unit_of_measurement + return "%" @property def device_state_attributes(self): """Return the state attributes of the battery.""" - attr = {ATTR_EVENT_ID: slugify(self._device.name)} + attr = {} + + if self._device.type in Switch.ZHATYPE: + for event in self.gateway.events: + if self._device == event.device: + attr[ATTR_EVENT_ID] = event.event_id + return attr From 9c2053a251c88999ae215171c568a5c2e27c32f2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 14 Sep 2019 22:53:59 +0200 Subject: [PATCH 025/296] deCONZ - Remove mechanisms to import a configuration from configuration.yaml (#26648) --- homeassistant/components/deconz/__init__.py | 36 +----- .../components/deconz/config_flow.py | 23 +--- tests/components/deconz/test_config_flow.py | 35 ----- tests/components/deconz/test_init.py | 122 +++++------------- 4 files changed, 36 insertions(+), 180 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index af9f619cb3e1bd..558b0fe4205147 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -1,48 +1,20 @@ """Support for deCONZ devices.""" import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import ( - CONF_API_KEY, - CONF_HOST, - CONF_PORT, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.helpers import config_validation as cv +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from .config_flow import get_master_gateway -from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DEFAULT_PORT, DOMAIN +from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DOMAIN from .gateway import DeconzGateway from .services import async_setup_services, async_unload_services CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } - ) - }, - extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.Schema({}, extra=vol.ALLOW_EXTRA)}, extra=vol.ALLOW_EXTRA ) async def async_setup(hass, config): - """Load configuration for deCONZ component. - - Discovery has loaded the component if DOMAIN is not present in config. - """ - if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: - deconz_config = config[DOMAIN] - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=deconz_config, - ) - ) + """Old way of setting up deCONZ integrations.""" return True diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 12e2e092f67848..c63b1721393222 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -191,31 +191,12 @@ async def async_step_ssdp(self, discovery_info): # pylint: disable=unsupported-assignment-operation self.context[CONF_BRIDGEID] = bridgeid - deconz_config = { + self.deconz_config = { CONF_HOST: discovery_info[CONF_HOST], CONF_PORT: discovery_info[CONF_PORT], } - return await self.async_step_import(deconz_config) - - async def async_step_import(self, import_config): - """Import a deCONZ bridge as a config entry. - - This flow is triggered by `async_setup` for configured bridges. - This flow is also triggered by `async_step_discovery`. - - This will execute for any bridge that does not have a - config entry yet (based on host). - - If an API key is provided, we will create an entry. - Otherwise we will delegate to `link` step which - will ask user to link the bridge. - """ - self.deconz_config = import_config - if CONF_API_KEY not in import_config: - return await self.async_step_link() - - return await self._create_entry() + return await self.async_step_link() async def async_step_hassio(self, user_input=None): """Prepare configuration for a Hass.io deCONZ bridge. diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 3f00c31c7e8746..d7071d6daef08c 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -234,41 +234,6 @@ async def test_bridge_discovery_update_existing_entry(hass): assert entry.data[config_flow.CONF_HOST] == "mock-deconz" -async def test_import_without_api_key(hass): - """Test importing a host without an API key.""" - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - data={config_flow.CONF_HOST: "1.2.3.4"}, - context={"source": "import"}, - ) - - assert result["type"] == "form" - assert result["step_id"] == "link" - - -async def test_import_with_api_key(hass): - """Test importing a host with an API key.""" - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - data={ - config_flow.CONF_BRIDGEID: "id", - config_flow.CONF_HOST: "mock-deconz", - config_flow.CONF_PORT: 80, - config_flow.CONF_API_KEY: "1234567890ABCDEF", - }, - context={"source": "import"}, - ) - - assert result["type"] == "create_entry" - assert result["title"] == "deCONZ-id" - assert result["data"] == { - config_flow.CONF_BRIDGEID: "id", - config_flow.CONF_HOST: "mock-deconz", - config_flow.CONF_PORT: 80, - config_flow.CONF_API_KEY: "1234567890ABCDEF", - } - - async def test_create_entry(hass, aioclient_mock): """Test that _create_entry work and that bridgeid can be requested.""" aioclient_mock.get( diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index b0456e0b6248bb..d0586565521716 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -6,7 +6,6 @@ import voluptuous as vol from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.setup import async_setup_component from homeassistant.components import deconz from tests.common import mock_coro, MockConfigEntry @@ -34,74 +33,13 @@ async def setup_entry(hass, entry): assert await deconz.async_setup_entry(hass, entry) is True -async def test_config_with_host_passed_to_config_entry(hass): - """Test that configured options for a host are loaded via config entry.""" - with patch.object(hass.config_entries, "flow") as mock_config_flow: - assert ( - await async_setup_component( - hass, - deconz.DOMAIN, - { - deconz.DOMAIN: { - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - } - }, - ) - is True - ) - # Import flow started - assert len(mock_config_flow.mock_calls) == 1 - - -async def test_config_without_host_not_passed_to_config_entry(hass): - """Test that a configuration without a host does not initiate an import.""" - MockConfigEntry(domain=deconz.DOMAIN, data={}).add_to_hass(hass) - with patch.object(hass.config_entries, "flow") as mock_config_flow: - assert ( - await async_setup_component(hass, deconz.DOMAIN, {deconz.DOMAIN: {}}) - is True - ) - # No flow started - assert len(mock_config_flow.mock_calls) == 0 - - -async def test_config_import_entry_fails_when_entries_exist(hass): - """Test that an already registered host does not initiate an import.""" - MockConfigEntry(domain=deconz.DOMAIN, data={}).add_to_hass(hass) - with patch.object(hass.config_entries, "flow") as mock_config_flow: - assert ( - await async_setup_component( - hass, - deconz.DOMAIN, - { - deconz.DOMAIN: { - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - } - }, - ) - is True - ) - # No flow started - assert len(mock_config_flow.mock_calls) == 0 - - -async def test_config_discovery(hass): - """Test that a discovered bridge does not initiate an import.""" - with patch.object(hass, "config_entries") as mock_config_entries: - assert await async_setup_component(hass, deconz.DOMAIN, {}) is True - # No flow started - assert len(mock_config_entries.flow.mock_calls) == 0 - - async def test_setup_entry_fails(hass): """Test setup entry fails if deCONZ is not available.""" entry = Mock() entry.data = { - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, } with patch("pydeconz.DeconzSession.async_load_parameters", side_effect=Exception): await deconz.async_setup_entry(hass, entry) @@ -111,9 +49,9 @@ async def test_setup_entry_no_available_bridge(hass): """Test setup entry fails if deCONZ is not available.""" entry = Mock() entry.data = { - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, } with patch( "pydeconz.DeconzSession.async_load_parameters", side_effect=asyncio.TimeoutError @@ -126,9 +64,9 @@ async def test_setup_entry_successful(hass): entry = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, }, ) @@ -145,9 +83,9 @@ async def test_setup_entry_multiple_gateways(hass): entry = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, }, ) @@ -156,9 +94,9 @@ async def test_setup_entry_multiple_gateways(hass): entry2 = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY2_HOST, - deconz.CONF_PORT: ENTRY2_PORT, - deconz.CONF_API_KEY: ENTRY2_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY2_HOST, + deconz.config_flow.CONF_PORT: ENTRY2_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY2_API_KEY, deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID, }, ) @@ -178,9 +116,9 @@ async def test_unload_entry(hass): entry = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, }, ) @@ -201,9 +139,9 @@ async def test_unload_entry_multiple_gateways(hass): entry = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, }, ) @@ -212,9 +150,9 @@ async def test_unload_entry_multiple_gateways(hass): entry2 = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY2_HOST, - deconz.CONF_PORT: ENTRY2_PORT, - deconz.CONF_API_KEY: ENTRY2_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY2_HOST, + deconz.config_flow.CONF_PORT: ENTRY2_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY2_API_KEY, deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID, }, ) @@ -237,9 +175,9 @@ async def test_service_configure(hass): entry = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, }, ) @@ -304,9 +242,9 @@ async def test_service_refresh_devices(hass): entry = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, }, ) From 57833f5b1e6e6e508c14d4db2635c203f0c2545a Mon Sep 17 00:00:00 2001 From: chriscla Date: Sat, 14 Sep 2019 17:44:19 -0700 Subject: [PATCH 026/296] Refactor nzbget to support future platform changes (#26462) * Re-factor nzbget platform to enable future features. * Re-factor nzbget platform to enable future features. * Re-factor nzbget platform to enable future features. * Re-factor nzbget platform to enable future features. * Use pynzbgetapi instead of raw HTTP requests * Using pynzbgetapi * Pinning pynzbgetapi version. * Requiring pynzbgetapi 0.2.0 * Addressing review comments * Refreshing requirements (adding pynzbgetapi) * Remove period from logging message * Updating requirements file * Add nzbget init to .coveragerc * Adding nzbget codeowner * Updating codeowners file --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/nzbget/__init__.py | 105 ++++++++++++ homeassistant/components/nzbget/manifest.json | 4 +- homeassistant/components/nzbget/sensor.py | 157 +++++------------- requirements_all.txt | 3 + 6 files changed, 151 insertions(+), 120 deletions(-) diff --git a/.coveragerc b/.coveragerc index fef77c8a247bba..824fb3828f2d32 100644 --- a/.coveragerc +++ b/.coveragerc @@ -436,6 +436,7 @@ omit = homeassistant/components/nuki/lock.py homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py + homeassistant/components/nzbget/__init__.py homeassistant/components/nzbget/sensor.py homeassistant/components/obihai/* homeassistant/components/octoprint/* diff --git a/CODEOWNERS b/CODEOWNERS index 18218bbf68e0f9..fe5e19f9115f37 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -197,6 +197,7 @@ homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuki/* @pschmitt homeassistant/components/nws/* @MatthewFlamm +homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/onboarding/* @home-assistant/core diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 2480daf2ead090..563fe2610932c0 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -1 +1,106 @@ """The nzbget component.""" +from datetime import timedelta +import logging + +import pynzbgetapi +import requests +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "nzbget" +DATA_NZBGET = "data_nzbget" +DATA_UPDATED = "nzbget_data_updated" + +DEFAULT_NAME = "NZBGet" +DEFAULT_PORT = 6789 + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=5) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + vol.Optional(CONF_SSL, default=False): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the NZBGet sensors.""" + host = config[DOMAIN][CONF_HOST] + port = config[DOMAIN][CONF_PORT] + ssl = "s" if config[DOMAIN][CONF_SSL] else "" + name = config[DOMAIN][CONF_NAME] + username = config[DOMAIN].get(CONF_USERNAME) + password = config[DOMAIN].get(CONF_PASSWORD) + scan_interval = config[DOMAIN][CONF_SCAN_INTERVAL] + + try: + nzbget_api = pynzbgetapi.NZBGetAPI(host, username, password, ssl, ssl, port) + nzbget_api.version() + except pynzbgetapi.NZBGetAPIException as conn_err: + _LOGGER.error("Error setting up NZBGet API: %s", conn_err) + return False + + _LOGGER.debug("Successfully validated NZBGet API connection") + + nzbget_data = hass.data[DATA_NZBGET] = NZBGetData(hass, nzbget_api) + nzbget_data.update() + + def refresh(event_time): + """Get the latest data from NZBGet.""" + nzbget_data.update() + + track_time_interval(hass, refresh, scan_interval) + + sensorconfig = {"client_name": name} + + hass.helpers.discovery.load_platform("sensor", DOMAIN, sensorconfig, config) + + return True + + +class NZBGetData: + """Get the latest data and update the states.""" + + def __init__(self, hass, api): + """Initialize the NZBGet RPC API.""" + self.hass = hass + self.status = None + self.available = True + self._api = api + + def update(self): + """Get the latest data from NZBGet instance.""" + try: + self.status = self._api.status() + self.available = True + dispatcher_send(self.hass, DATA_UPDATED) + except requests.exceptions.ConnectionError: + self.available = False + _LOGGER.error("Unable to refresh NZBGet data") diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json index 69293ede516aee..17b11d6aef9fdc 100644 --- a/homeassistant/components/nzbget/manifest.json +++ b/homeassistant/components/nzbget/manifest.json @@ -2,7 +2,7 @@ "domain": "nzbget", "name": "Nzbget", "documentation": "https://www.home-assistant.io/components/nzbget", - "requirements": [], + "requirements": ["pynzbgetapi==0.2.0"], "dependencies": [], - "codeowners": [] + "codeowners": ["@chriscla"] } diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 73643a5383cea1..ce1fda0839e10b 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -1,32 +1,15 @@ -"""Support for monitoring NZBGet NZB client.""" -from datetime import timedelta +"""Monitor the NZBGet API.""" import logging -from aiohttp.hdrs import CONTENT_TYPE -import requests -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_SSL, - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_PASSWORD, - CONF_USERNAME, - CONTENT_TYPE_JSON, - CONF_MONITORED_VARIABLES, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle + +from . import DATA_NZBGET, DATA_UPDATED _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "NZBGet" -DEFAULT_PORT = 6789 - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) SENSOR_TYPES = { "article_cache": ["ArticleCacheMB", "Article Cache", "MB"], @@ -40,66 +23,39 @@ "uptime": ["UpTimeSec", "Uptime", "min"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=["download_rate"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_USERNAME): cv.string, - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the NZBGet sensors.""" - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - ssl = "s" if config.get(CONF_SSL) else "" - name = config.get(CONF_NAME) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - monitored_types = config.get(CONF_MONITORED_VARIABLES) - - url = f"http{ssl}://{host}:{port}/jsonrpc" - - try: - nzbgetapi = NZBGetAPI(api_url=url, username=username, password=password) - nzbgetapi.update() - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as conn_err: - _LOGGER.error("Error setting up NZBGet API: %s", conn_err) - return False + """Create NZBGet sensors.""" + + if discovery_info is None: + return + + nzbget_data = hass.data[DATA_NZBGET] + name = discovery_info["client_name"] devices = [] - for ng_type in monitored_types: + for sensor_type, sensor_config in SENSOR_TYPES.items(): new_sensor = NZBGetSensor( - api=nzbgetapi, sensor_type=SENSOR_TYPES.get(ng_type), client_name=name + nzbget_data, sensor_type, name, sensor_config[0], sensor_config[1] ) devices.append(new_sensor) - add_entities(devices) + add_entities(devices, True) class NZBGetSensor(Entity): """Representation of a NZBGet sensor.""" - def __init__(self, api, sensor_type, client_name): + def __init__( + self, nzbget_data, sensor_type, client_name, sensor_name, unit_of_measurement + ): """Initialize a new NZBGet sensor.""" - self._name = "{} {}".format(client_name, sensor_type[1]) - self.type = sensor_type[0] + self._name = f"{client_name} {sensor_type}" + self.type = sensor_name self.client_name = client_name - self.api = api + self.nzbget_data = nzbget_data self._state = None - self._unit_of_measurement = sensor_type[2] - self.update() - _LOGGER.debug("Created NZBGet sensor: %s", self.type) + self._unit_of_measurement = unit_of_measurement @property def name(self): @@ -116,21 +72,31 @@ def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement + @property + def available(self): + """Return whether the sensor is available.""" + return self.nzbget_data.available + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + @callback + def _schedule_immediate_update(self): + self.async_schedule_update_ha_state(True) + def update(self): """Update state of sensor.""" - try: - self.api.update() - except requests.exceptions.ConnectionError: - # Error calling the API, already logged in api.update() - return - if self.api.status is None: + if self.nzbget_data.status is None: _LOGGER.debug( "Update of %s requested, but no status is available", self._name ) return - value = self.api.status.get(self.type) + value = self.nzbget_data.status.get(self.type) if value is None: _LOGGER.warning("Unable to locate value for %s", self.type) return @@ -143,48 +109,3 @@ def update(self): self._state = round(value / 60, 2) else: self._state = value - - -class NZBGetAPI: - """Simple JSON-RPC wrapper for NZBGet's API.""" - - def __init__(self, api_url, username=None, password=None): - """Initialize NZBGet API and set headers needed later.""" - self.api_url = api_url - self.status = None - self.headers = {CONTENT_TYPE: CONTENT_TYPE_JSON} - - if username is not None and password is not None: - self.auth = (username, password) - else: - self.auth = None - self.update() - - def post(self, method, params=None): - """Send a POST request and return the response as a dict.""" - payload = {"method": method} - - if params: - payload["params"] = params - try: - response = requests.post( - self.api_url, - json=payload, - auth=self.auth, - headers=self.headers, - timeout=5, - ) - response.raise_for_status() - return response.json() - except requests.exceptions.ConnectionError as conn_exc: - _LOGGER.error( - "Failed to update NZBGet status from %s. Error: %s", - self.api_url, - conn_exc, - ) - raise - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update cached response.""" - self.status = self.post("status")["result"] diff --git a/requirements_all.txt b/requirements_all.txt index 2075dab9a371e3..e5ecd69ee2469a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1347,6 +1347,9 @@ pynws==0.7.4 # homeassistant.components.nx584 pynx584==0.4 +# homeassistant.components.nzbget +pynzbgetapi==0.2.0 + # homeassistant.components.obihai pyobihai==1.0.2 From 6a60ebdb30d164de7eca9ad4dccef5f6d4955c02 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 15 Sep 2019 09:50:17 +0200 Subject: [PATCH 027/296] Rename MockToggleDevice to MockToggleEntity (#26644) * Rename MockToggleDevice to MockToggleEntity * Fix tests --- tests/common.py | 16 +-- tests/components/flux/test_switch.py | 110 +++++++-------- .../generic_thermostat/test_climate.py | 2 +- .../light/test_device_automation.py | 48 +++---- tests/components/light/test_init.py | 130 +++++++++--------- tests/components/scene/test_init.py | 2 +- .../switch/test_device_automation.py | 48 +++---- tests/components/switch/test_init.py | 2 +- .../custom_components/test/light.py | 20 +-- .../custom_components/test/switch.py | 20 +-- 10 files changed, 200 insertions(+), 198 deletions(-) diff --git a/tests/common.py b/tests/common.py index 847635d4dad67e..fda5c743222703 100644 --- a/tests/common.py +++ b/tests/common.py @@ -602,40 +602,40 @@ def __init__( ) -class MockToggleDevice(entity.ToggleEntity): +class MockToggleEntity(entity.ToggleEntity): """Provide a mock toggle device.""" - def __init__(self, name, state): - """Initialize the mock device.""" + def __init__(self, name, state, unique_id=None): + """Initialize the mock entity.""" self._name = name or DEVICE_DEFAULT_NAME self._state = state self.calls = [] @property def name(self): - """Return the name of the device if any.""" + """Return the name of the entity if any.""" self.calls.append(("name", {})) return self._name @property def state(self): - """Return the name of the device if any.""" + """Return the state of the entity if any.""" self.calls.append(("state", {})) return self._state @property def is_on(self): - """Return true if device is on.""" + """Return true if entity is on.""" self.calls.append(("is_on", {})) return self._state == STATE_ON def turn_on(self, **kwargs): - """Turn the device on.""" + """Turn the entity on.""" self.calls.append(("turn_on", kwargs)) self._state = STATE_ON def turn_off(self, **kwargs): - """Turn the device off.""" + """Turn the entity off.""" self.calls.append(("turn_off", kwargs)) self._state = STATE_OFF diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index 08a49c4a6670d4..fb35485f5c9685 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -82,10 +82,10 @@ async def test_flux_when_switch_is_off(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -113,7 +113,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], } }, ) @@ -131,10 +131,10 @@ async def test_flux_before_sunrise(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -162,7 +162,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], } }, ) @@ -184,10 +184,10 @@ async def test_flux_before_sunrise_known_location(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -210,7 +210,7 @@ async def test_flux_before_sunrise_known_location(hass): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], # 'brightness': 255, # 'disable_brightness_adjust': True, # 'mode': 'rgb', @@ -237,10 +237,10 @@ async def test_flux_after_sunrise_before_sunset(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -267,7 +267,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], } }, ) @@ -290,10 +290,10 @@ async def test_flux_after_sunset_before_stop(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -320,7 +320,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "22:00", } }, @@ -344,10 +344,10 @@ async def test_flux_after_stop_before_sunrise(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -374,7 +374,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], } }, ) @@ -397,10 +397,10 @@ async def test_flux_with_custom_start_stop_times(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -427,7 +427,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "start_time": "6:00", "stop_time": "23:30", } @@ -454,10 +454,10 @@ async def test_flux_before_sunrise_stop_next_day(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -484,7 +484,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -512,10 +512,10 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -542,7 +542,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -570,10 +570,10 @@ async def test_flux_after_sunset_before_midnight_stop_next_day(hass, x): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -600,7 +600,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -627,10 +627,10 @@ async def test_flux_after_sunset_after_midnight_stop_next_day(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -657,7 +657,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -684,10 +684,10 @@ async def test_flux_after_stop_before_sunrise_stop_next_day(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -714,7 +714,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -738,10 +738,10 @@ async def test_flux_with_custom_colortemps(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -768,7 +768,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "start_colortemp": "1000", "stop_colortemp": "6000", "stop_time": "22:00", @@ -794,10 +794,10 @@ async def test_flux_with_custom_brightness(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -824,7 +824,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "brightness": 255, "stop_time": "22:00", } @@ -848,23 +848,23 @@ async def test_flux_with_multiple_lights(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1, dev2, dev3 = platform.DEVICES - common_light.turn_on(hass, entity_id=dev2.entity_id) + ent1, ent2, ent3 = platform.ENTITIES + common_light.turn_on(hass, entity_id=ent2.entity_id) await hass.async_block_till_done() - common_light.turn_on(hass, entity_id=dev3.entity_id) + common_light.turn_on(hass, entity_id=ent3.entity_id) await hass.async_block_till_done() - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None - state = hass.states.get(dev2.entity_id) + state = hass.states.get(ent2.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None - state = hass.states.get(dev3.entity_id) + state = hass.states.get(ent3.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -893,7 +893,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id, dev2.entity_id, dev3.entity_id], + "lights": [ent1.entity_id, ent2.entity_id, ent3.entity_id], } }, ) @@ -921,10 +921,10 @@ async def test_flux_with_mired(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("color_temp") is None @@ -950,7 +950,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "mode": "mired", } }, @@ -972,10 +972,10 @@ async def test_flux_with_rgb(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("color_temp") is None @@ -1001,7 +1001,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "mode": "rgb", } }, diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 367ea52b3a2bcd..776d8f39f69613 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -116,7 +116,7 @@ async def test_heater_switch(hass, setup_comp_1): """Test heater switching test switch.""" platform = getattr(hass.components, "test.switch") platform.init() - switch_1 = platform.DEVICES[1] + switch_1 = platform.ENTITIES[1] assert await async_setup_component( hass, switch.DOMAIN, {"switch": {"platform": "test"}} ) diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py index 40fa08856c5684..3525f1121c08e2 100644 --- a/tests/components/light/test_device_automation.py +++ b/tests/components/light/test_device_automation.py @@ -150,7 +150,7 @@ async def test_if_fires_on_state_change(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -162,7 +162,7 @@ async def test_if_fires_on_state_change(hass, calls): "platform": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_on", }, "action": { @@ -186,7 +186,7 @@ async def test_if_fires_on_state_change(hass, calls): "platform": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_off", }, "action": { @@ -209,21 +209,21 @@ async def test_if_fires_on_state_change(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 - hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.states.async_set(ent1.entity_id, STATE_OFF) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( - dev1.entity_id + ent1.entity_id ) - hass.states.async_set(dev1.entity_id, STATE_ON) + hass.states.async_set(ent1.entity_id, STATE_ON) await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( - dev1.entity_id + ent1.entity_id ) @@ -234,7 +234,7 @@ async def test_if_state(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -248,7 +248,7 @@ async def test_if_state(hass, calls): "condition": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "is_on", } ], @@ -267,7 +267,7 @@ async def test_if_state(hass, calls): "condition": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "is_off", } ], @@ -283,7 +283,7 @@ async def test_if_state(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 hass.bus.async_fire("test_event1") @@ -292,7 +292,7 @@ async def test_if_state(hass, calls): assert len(calls) == 1 assert calls[0].data["some"] == "is_on event - test_event1" - hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.states.async_set(ent1.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() @@ -307,7 +307,7 @@ async def test_action(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -319,7 +319,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_off", }, }, @@ -328,7 +328,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_on", }, }, @@ -337,7 +337,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "toggle", }, }, @@ -345,29 +345,29 @@ async def test_action(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index dc4cb7502c5765..8ceda6cbd3efa7 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -137,39 +137,39 @@ def test_services(self): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES # Test init - assert light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) # Test basic turn_on, turn_off, toggle services - common.turn_off(self.hass, entity_id=dev1.entity_id) - common.turn_on(self.hass, entity_id=dev2.entity_id) + common.turn_off(self.hass, entity_id=ent1.entity_id) + common.turn_on(self.hass, entity_id=ent2.entity_id) self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert light.is_on(self.hass, dev2.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert light.is_on(self.hass, ent2.entity_id) # turn on all lights common.turn_on(self.hass) self.hass.block_till_done() - assert light.is_on(self.hass, dev1.entity_id) - assert light.is_on(self.hass, dev2.entity_id) - assert light.is_on(self.hass, dev3.entity_id) + assert light.is_on(self.hass, ent1.entity_id) + assert light.is_on(self.hass, ent2.entity_id) + assert light.is_on(self.hass, ent3.entity_id) # turn off all lights common.turn_off(self.hass) self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) # turn off all lights by setting brightness to 0 common.turn_on(self.hass) @@ -180,97 +180,97 @@ def test_services(self): self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) # toggle all lights common.toggle(self.hass) self.hass.block_till_done() - assert light.is_on(self.hass, dev1.entity_id) - assert light.is_on(self.hass, dev2.entity_id) - assert light.is_on(self.hass, dev3.entity_id) + assert light.is_on(self.hass, ent1.entity_id) + assert light.is_on(self.hass, ent2.entity_id) + assert light.is_on(self.hass, ent3.entity_id) # toggle all lights common.toggle(self.hass) self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) # Ensure all attributes process correctly common.turn_on( - self.hass, dev1.entity_id, transition=10, brightness=20, color_name="blue" + self.hass, ent1.entity_id, transition=10, brightness=20, color_name="blue" ) common.turn_on( - self.hass, dev2.entity_id, rgb_color=(255, 255, 255), white_value=255 + self.hass, ent2.entity_id, rgb_color=(255, 255, 255), white_value=255 ) - common.turn_on(self.hass, dev3.entity_id, xy_color=(0.4, 0.6)) + common.turn_on(self.hass, ent3.entity_id, xy_color=(0.4, 0.6)) self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") assert { light.ATTR_TRANSITION: 10, light.ATTR_BRIGHTNESS: 20, light.ATTR_HS_COLOR: (240, 100), } == data - _, data = dev2.last_call("turn_on") + _, data = ent2.last_call("turn_on") assert {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255} == data - _, data = dev3.last_call("turn_on") + _, data = ent3.last_call("turn_on") assert {light.ATTR_HS_COLOR: (71.059, 100)} == data # Ensure attributes are filtered when light is turned off common.turn_on( - self.hass, dev1.entity_id, transition=10, brightness=0, color_name="blue" + self.hass, ent1.entity_id, transition=10, brightness=0, color_name="blue" ) common.turn_on( self.hass, - dev2.entity_id, + ent2.entity_id, brightness=0, rgb_color=(255, 255, 255), white_value=0, ) - common.turn_on(self.hass, dev3.entity_id, brightness=0, xy_color=(0.4, 0.6)) + common.turn_on(self.hass, ent3.entity_id, brightness=0, xy_color=(0.4, 0.6)) self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) - _, data = dev1.last_call("turn_off") + _, data = ent1.last_call("turn_off") assert {light.ATTR_TRANSITION: 10} == data - _, data = dev2.last_call("turn_off") + _, data = ent2.last_call("turn_off") assert {} == data - _, data = dev3.last_call("turn_off") + _, data = ent3.last_call("turn_off") assert {} == data # One of the light profiles prof_name, prof_h, prof_s, prof_bri = "relax", 35.932, 69.412, 144 # Test light profiles - common.turn_on(self.hass, dev1.entity_id, profile=prof_name) + common.turn_on(self.hass, ent1.entity_id, profile=prof_name) # Specify a profile and a brightness attribute to overwrite it - common.turn_on(self.hass, dev2.entity_id, profile=prof_name, brightness=100) + common.turn_on(self.hass, ent2.entity_id, profile=prof_name, brightness=100) self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") assert { light.ATTR_BRIGHTNESS: prof_bri, light.ATTR_HS_COLOR: (prof_h, prof_s), } == data - _, data = dev2.last_call("turn_on") + _, data = ent2.last_call("turn_on") assert { light.ATTR_BRIGHTNESS: 100, light.ATTR_HS_COLOR: (prof_h, prof_s), @@ -278,34 +278,34 @@ def test_services(self): # Test bad data common.turn_on(self.hass) - common.turn_on(self.hass, dev1.entity_id, profile="nonexisting") - common.turn_on(self.hass, dev2.entity_id, xy_color=["bla-di-bla", 5]) - common.turn_on(self.hass, dev3.entity_id, rgb_color=[255, None, 2]) + common.turn_on(self.hass, ent1.entity_id, profile="nonexisting") + common.turn_on(self.hass, ent2.entity_id, xy_color=["bla-di-bla", 5]) + common.turn_on(self.hass, ent3.entity_id, rgb_color=[255, None, 2]) self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") assert {} == data - _, data = dev2.last_call("turn_on") + _, data = ent2.last_call("turn_on") assert {} == data - _, data = dev3.last_call("turn_on") + _, data = ent3.last_call("turn_on") assert {} == data # faulty attributes will not trigger a service call common.turn_on( - self.hass, dev1.entity_id, profile=prof_name, brightness="bright" + self.hass, ent1.entity_id, profile=prof_name, brightness="bright" ) - common.turn_on(self.hass, dev1.entity_id, rgb_color="yellowish") - common.turn_on(self.hass, dev2.entity_id, white_value="high") + common.turn_on(self.hass, ent1.entity_id, rgb_color="yellowish") + common.turn_on(self.hass, ent2.entity_id, white_value="high") self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") assert {} == data - _, data = dev2.last_call("turn_on") + _, data = ent2.last_call("turn_on") assert {} == data def test_broken_light_profiles(self): @@ -340,24 +340,24 @@ def test_light_profiles(self): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1, _, _ = platform.DEVICES + ent1, _, _ = platform.ENTITIES - common.turn_on(self.hass, dev1.entity_id, profile="test") + common.turn_on(self.hass, ent1.entity_id, profile="test") self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") - assert light.is_on(self.hass, dev1.entity_id) + assert light.is_on(self.hass, ent1.entity_id) assert {light.ATTR_HS_COLOR: (71.059, 100), light.ATTR_BRIGHTNESS: 100} == data - common.turn_on(self.hass, dev1.entity_id, profile="test_off") + common.turn_on(self.hass, ent1.entity_id, profile="test_off") self.hass.block_till_done() - _, data = dev1.last_call("turn_off") + _, data = ent1.last_call("turn_off") - assert not light.is_on(self.hass, dev1.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) assert {} == data def test_default_profiles_group(self): @@ -387,10 +387,10 @@ def _mock_open(path, *args, **kwargs): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev, _, _ = platform.DEVICES - common.turn_on(self.hass, dev.entity_id) + ent, _, _ = platform.ENTITIES + common.turn_on(self.hass, ent.entity_id) self.hass.block_till_done() - _, data = dev.last_call("turn_on") + _, data = ent.last_call("turn_on") assert {light.ATTR_HS_COLOR: (71.059, 100), light.ATTR_BRIGHTNESS: 99} == data def test_default_profiles_light(self): @@ -424,7 +424,9 @@ def _mock_open(path, *args, **kwargs): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", platform.DEVICES)) + dev = next( + filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES) + ) common.turn_on(self.hass, dev.entity_id) self.hass.block_till_done() _, data = dev.last_call("turn_on") diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 7047e6e8d92f3c..5c8d46cb727753 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -24,7 +24,7 @@ def setUp(self): # pylint: disable=invalid-name self.hass, light.DOMAIN, {light.DOMAIN: {"platform": "test"}} ) - self.light_1, self.light_2 = test_light.DEVICES[0:2] + self.light_1, self.light_2 = test_light.ENTITIES[0:2] common_light.turn_off( self.hass, [self.light_1.entity_id, self.light_2.entity_id] diff --git a/tests/components/switch/test_device_automation.py b/tests/components/switch/test_device_automation.py index 7dba73476514f0..3bd29a72a12549 100644 --- a/tests/components/switch/test_device_automation.py +++ b/tests/components/switch/test_device_automation.py @@ -150,7 +150,7 @@ async def test_if_fires_on_state_change(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -162,7 +162,7 @@ async def test_if_fires_on_state_change(hass, calls): "platform": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_on", }, "action": { @@ -186,7 +186,7 @@ async def test_if_fires_on_state_change(hass, calls): "platform": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_off", }, "action": { @@ -209,21 +209,21 @@ async def test_if_fires_on_state_change(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 - hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.states.async_set(ent1.entity_id, STATE_OFF) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( - dev1.entity_id + ent1.entity_id ) - hass.states.async_set(dev1.entity_id, STATE_ON) + hass.states.async_set(ent1.entity_id, STATE_ON) await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( - dev1.entity_id + ent1.entity_id ) @@ -234,7 +234,7 @@ async def test_if_state(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -248,7 +248,7 @@ async def test_if_state(hass, calls): "condition": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "is_on", } ], @@ -267,7 +267,7 @@ async def test_if_state(hass, calls): "condition": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "is_off", } ], @@ -283,7 +283,7 @@ async def test_if_state(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 hass.bus.async_fire("test_event1") @@ -292,7 +292,7 @@ async def test_if_state(hass, calls): assert len(calls) == 1 assert calls[0].data["some"] == "is_on event - test_event1" - hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.states.async_set(ent1.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() @@ -307,7 +307,7 @@ async def test_action(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -319,7 +319,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_off", }, }, @@ -328,7 +328,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_on", }, }, @@ -337,7 +337,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "toggle", }, }, @@ -345,29 +345,29 @@ async def test_action(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index c04a30589edd8f..a9463cb78f4fea 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -21,7 +21,7 @@ def setUp(self): platform = getattr(self.hass.components, "test.switch") platform.init() # Switch 1 is ON, switch 2 is OFF - self.switch_1, self.switch_2, self.switch_3 = platform.DEVICES + self.switch_1, self.switch_2, self.switch_3 = platform.ENTITIES # pylint: disable=invalid-name def tearDown(self): diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 43338c9e14ed1f..0a48388b718b1e 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -4,23 +4,23 @@ Call init before using it in your tests to ensure clean test data. """ from homeassistant.const import STATE_ON, STATE_OFF -from tests.common import MockToggleDevice +from tests.common import MockToggleEntity -DEVICES = [] +ENTITIES = [] def init(empty=False): - """Initialize the platform with devices.""" - global DEVICES + """Initialize the platform with entities.""" + global ENTITIES - DEVICES = ( + ENTITIES = ( [] if empty else [ - MockToggleDevice("Ceiling", STATE_ON), - MockToggleDevice("Ceiling", STATE_OFF), - MockToggleDevice(None, STATE_OFF), + MockToggleEntity("Ceiling", STATE_ON), + MockToggleEntity("Ceiling", STATE_OFF), + MockToggleEntity(None, STATE_OFF), ] ) @@ -28,5 +28,5 @@ def init(empty=False): async def async_setup_platform( hass, config, async_add_entities_callback, discovery_info=None ): - """Return mock devices.""" - async_add_entities_callback(DEVICES) + """Return mock entities.""" + async_add_entities_callback(ENTITIES) diff --git a/tests/testing_config/custom_components/test/switch.py b/tests/testing_config/custom_components/test/switch.py index f4226ecc63014f..484c47d1190e3c 100644 --- a/tests/testing_config/custom_components/test/switch.py +++ b/tests/testing_config/custom_components/test/switch.py @@ -4,23 +4,23 @@ Call init before using it in your tests to ensure clean test data. """ from homeassistant.const import STATE_ON, STATE_OFF -from tests.common import MockToggleDevice +from tests.common import MockToggleEntity -DEVICES = [] +ENTITIES = [] def init(empty=False): - """Initialize the platform with devices.""" - global DEVICES + """Initialize the platform with entities.""" + global ENTITIES - DEVICES = ( + ENTITIES = ( [] if empty else [ - MockToggleDevice("AC", STATE_ON), - MockToggleDevice("AC", STATE_OFF), - MockToggleDevice(None, STATE_OFF), + MockToggleEntity("AC", STATE_ON), + MockToggleEntity("AC", STATE_OFF), + MockToggleEntity(None, STATE_OFF), ] ) @@ -28,5 +28,5 @@ def init(empty=False): async def async_setup_platform( hass, config, async_add_entities_callback, discovery_info=None ): - """Find and return test switches.""" - async_add_entities_callback(DEVICES) + """Return mock entities.""" + async_add_entities_callback(ENTITIES) From fd359c622241a0ce39005e40f5a88e9c6db9ac88 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Sun, 15 Sep 2019 05:55:20 -0400 Subject: [PATCH 028/296] Fix Environment Canada weather forecast, retain icon_code sensor (#26646) * Bump env_canada to 0.0.25 * Keep icon_code --- homeassistant/components/environment_canada/manifest.json | 2 +- homeassistant/components/environment_canada/sensor.py | 1 - requirements_all.txt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 0625fd4c27f6d9..2ae2006512b03a 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -3,7 +3,7 @@ "name": "Environment Canada", "documentation": "https://www.home-assistant.io/components/environment_canada", "requirements": [ - "env_canada==0.0.24" + "env_canada==0.0.25" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 2413edaebce060..244fda61656f88 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -68,7 +68,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ec_data = ECData(coordinates=(lat, lon), language=config.get(CONF_LANGUAGE)) sensor_list = list(ec_data.conditions.keys()) + list(ec_data.alerts.keys()) - sensor_list.remove("icon_code") add_entities([ECSensor(sensor_type, ec_data) for sensor_type in sensor_list], True) diff --git a/requirements_all.txt b/requirements_all.txt index e5ecd69ee2469a..056b51c345774c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -446,7 +446,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.24 +env_canada==0.0.25 # homeassistant.components.envirophat # envirophat==0.0.6 From f45f8f2f3dd8f4587ea0e419c78ac0dbf81289f0 Mon Sep 17 00:00:00 2001 From: Bryan York Date: Sun, 15 Sep 2019 11:53:05 -0700 Subject: [PATCH 029/296] Emulate color temperature for non-ct lights in light groups (#23495) * Emulate color temperature for non-ct lights in light groups * fix tests * Address review comments * Fix black formatting * Fix for pylint * Address comments * Fix black formatting * Address comments --- .../components/google_assistant/trait.py | 2 +- homeassistant/components/group/light.py | 48 +++++++++++++++++- tests/components/group/test_light.py | 50 +++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 5fa7d49b885b1c..2afa18af32e6a7 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -308,7 +308,7 @@ def sync_attributes(self): if features & light.SUPPORT_COLOR_TEMP: # Max Kelvin is Min Mireds K = 1000000 / mireds - # Min Kevin is Max Mireds K = 1000000 / mireds + # Min Kelvin is Max Mireds K = 1000000 / mireds response["colorTemperatureRange"] = { "temperatureMaxK": color_util.color_temperature_mired_to_kelvin( attrs.get(light.ATTR_MIN_MIREDS) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 87d8134ccbf6b3..0b1291d4045f42 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -1,4 +1,5 @@ """This platform allows several lights to be grouped into one light.""" +import asyncio from collections import Counter import itertools import logging @@ -19,6 +20,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -179,6 +181,7 @@ def should_poll(self) -> bool: async def async_turn_on(self, **kwargs): """Forward the turn_on command to all lights in the light group.""" data = {ATTR_ENTITY_ID: self._entity_ids} + emulate_color_temp_entity_ids = [] if ATTR_BRIGHTNESS in kwargs: data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] @@ -189,6 +192,23 @@ async def async_turn_on(self, **kwargs): if ATTR_COLOR_TEMP in kwargs: data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP] + # Create a new entity list to mutate + updated_entities = list(self._entity_ids) + + # Walk through initial entity ids, split entity lists by support + for entity_id in self._entity_ids: + state = self.hass.states.get(entity_id) + if not state: + continue + support = state.attributes.get(ATTR_SUPPORTED_FEATURES) + # Only pass color temperature to supported entity_ids + if bool(support & SUPPORT_COLOR) and not bool( + support & SUPPORT_COLOR_TEMP + ): + emulate_color_temp_entity_ids.append(entity_id) + updated_entities.remove(entity_id) + data[ATTR_ENTITY_ID] = updated_entities + if ATTR_WHITE_VALUE in kwargs: data[ATTR_WHITE_VALUE] = kwargs[ATTR_WHITE_VALUE] @@ -201,8 +221,32 @@ async def async_turn_on(self, **kwargs): if ATTR_FLASH in kwargs: data[ATTR_FLASH] = kwargs[ATTR_FLASH] - await self.hass.services.async_call( - light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True + if not emulate_color_temp_entity_ids: + await self.hass.services.async_call( + light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True + ) + return + + emulate_color_temp_data = data.copy() + temp_k = color_util.color_temperature_mired_to_kelvin( + emulate_color_temp_data[ATTR_COLOR_TEMP] + ) + hs_color = color_util.color_temperature_to_hs(temp_k) + emulate_color_temp_data[ATTR_HS_COLOR] = hs_color + del emulate_color_temp_data[ATTR_COLOR_TEMP] + + emulate_color_temp_data[ATTR_ENTITY_ID] = emulate_color_temp_entity_ids + + await asyncio.gather( + self.hass.services.async_call( + light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True + ), + self.hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_ON, + emulate_color_temp_data, + blocking=True, + ), ) async def async_turn_off(self, **kwargs): diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index d3b0d8dd3010d3..87898e42d593e9 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -186,6 +186,56 @@ async def test_color_temp(hass): assert state.attributes["color_temp"] == 1000 +async def test_emulated_color_temp_group(hass): + """Test emulated color temperature in a group.""" + await async_setup_component( + hass, + "light", + { + "light": [ + {"platform": "demo"}, + { + "platform": "group", + "entities": [ + "light.bed_light", + "light.ceiling_lights", + "light.kitchen_lights", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + + hass.states.async_set("light.bed_light", "on", {"supported_features": 2}) + await hass.async_block_till_done() + hass.states.async_set("light.ceiling_lights", "on", {"supported_features": 63}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen_lights", "on", {"supported_features": 61}) + await hass.async_block_till_done() + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.light_group", "color_temp": 200}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.bed_light") + assert state.state == "on" + assert state.attributes["color_temp"] == 200 + assert "hs_color" not in state.attributes.keys() + + state = hass.states.get("light.ceiling_lights") + assert state.state == "on" + assert state.attributes["color_temp"] == 200 + assert "hs_color" in state.attributes.keys() + + state = hass.states.get("light.kitchen_lights") + assert state.state == "on" + assert state.attributes["hs_color"] == (27.001, 19.243) + + async def test_min_max_mireds(hass): """Test min/max mireds reporting.""" await async_setup_component( From 719a6018805c41fac99fa262a40e126cd7f4d8c8 Mon Sep 17 00:00:00 2001 From: chriscla Date: Sun, 15 Sep 2019 22:06:21 -0700 Subject: [PATCH 030/296] Use pynzbgetapi exceptions consistently (#26667) --- homeassistant/components/nzbget/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 563fe2610932c0..37744dce180342 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -3,7 +3,6 @@ import logging import pynzbgetapi -import requests import voluptuous as vol from homeassistant.const import ( @@ -101,6 +100,6 @@ def update(self): self.status = self._api.status() self.available = True dispatcher_send(self.hass, DATA_UPDATED) - except requests.exceptions.ConnectionError: + except pynzbgetapi.NZBGetAPIException: self.available = False _LOGGER.error("Unable to refresh NZBGet data") From 5116d02747ae3f549966c4ba5789ec051679f7cb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 16 Sep 2019 10:08:13 +0200 Subject: [PATCH 031/296] deCONZ - Improve service tests (#26663) * Improve configure service tests * Add refresh device service test * Add tests for setup and unload services * Remove refresh device test from test_init * Extra verification of deconz services existance in hass.data --- homeassistant/components/deconz/services.py | 5 +- tests/components/deconz/test_init.py | 96 -------- tests/components/deconz/test_services.py | 245 ++++++++++++++++++++ 3 files changed, 248 insertions(+), 98 deletions(-) create mode 100644 tests/components/deconz/test_services.py diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 31ba0ff3581260..3498b46d879c8f 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -89,13 +89,14 @@ async def async_configure_service(hass, data): See Dresden Elektroniks REST API documentation for details: http://dresden-elektronik.github.io/deconz-rest-doc/rest/ """ + bridgeid = data.get(CONF_BRIDGEID) field = data.get(SERVICE_FIELD, "") entity_id = data.get(SERVICE_ENTITY) data = data[SERVICE_DATA] gateway = get_master_gateway(hass) - if CONF_BRIDGEID in data: - gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]] + if bridgeid: + gateway = hass.data[DOMAIN][bridgeid] if entity_id: try: diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index d0586565521716..7d630498cde14e 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -3,7 +3,6 @@ import asyncio import pytest -import voluptuous as vol from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components import deconz @@ -168,98 +167,3 @@ async def test_unload_entry_multiple_gateways(hass): assert ENTRY2_BRIDGEID in hass.data[deconz.DOMAIN] assert hass.data[deconz.DOMAIN][ENTRY2_BRIDGEID].master - - -async def test_service_configure(hass): - """Test that service invokes pydeconz with the correct path and data.""" - entry = MockConfigEntry( - domain=deconz.DOMAIN, - data={ - deconz.config_flow.CONF_HOST: ENTRY1_HOST, - deconz.config_flow.CONF_PORT: ENTRY1_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, - deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, - }, - ) - entry.add_to_hass(hass) - - await setup_entry(hass, entry) - - hass.data[deconz.DOMAIN][ENTRY1_BRIDGEID].deconz_ids = {"light.test": "/light/1"} - data = {"on": True, "attr1": 10, "attr2": 20} - - # only field - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - await hass.services.async_call( - "deconz", "configure", service_data={"field": "/light/42", "data": data} - ) - await hass.async_block_till_done() - - # only entity - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - await hass.services.async_call( - "deconz", "configure", service_data={"entity": "light.test", "data": data} - ) - await hass.async_block_till_done() - - # entity + field - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - await hass.services.async_call( - "deconz", - "configure", - service_data={"entity": "light.test", "field": "/state", "data": data}, - ) - await hass.async_block_till_done() - - # non-existing entity (or not from deCONZ) - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - await hass.services.async_call( - "deconz", - "configure", - service_data={ - "entity": "light.nonexisting", - "field": "/state", - "data": data, - }, - ) - await hass.async_block_till_done() - - # field does not start with / - with pytest.raises(vol.Invalid): - with patch( - "pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True) - ): - await hass.services.async_call( - "deconz", - "configure", - service_data={"entity": "light.test", "field": "state", "data": data}, - ) - await hass.async_block_till_done() - - -async def test_service_refresh_devices(hass): - """Test that service can refresh devices.""" - entry = MockConfigEntry( - domain=deconz.DOMAIN, - data={ - deconz.config_flow.CONF_HOST: ENTRY1_HOST, - deconz.config_flow.CONF_PORT: ENTRY1_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, - deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, - }, - ) - entry.add_to_hass(hass) - - await setup_entry(hass, entry) - - with patch( - "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(True) - ): - await hass.services.async_call("deconz", "device_refresh", service_data={}) - await hass.async_block_till_done() - - with patch( - "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(False) - ): - await hass.services.async_call("deconz", "device_refresh", service_data={}) - await hass.async_block_till_done() diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py new file mode 100644 index 00000000000000..63934871fcbf39 --- /dev/null +++ b/tests/components/deconz/test_services.py @@ -0,0 +1,245 @@ +"""deCONZ service tests.""" +from asynctest import Mock, patch + +import pytest +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import deconz + +BRIDGEID = "0123456789" + +ENTRY_CONFIG = { + deconz.config_flow.CONF_API_KEY: "ABCDEF", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, + deconz.config_flow.CONF_HOST: "1.2.3.4", + deconz.config_flow.CONF_PORT: 80, +} + +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, +} + +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} + +GROUP = { + "1": { + "id": "Group 1 id", + "name": "Group 1 name", + "type": "LightGroup", + "state": {}, + "action": {}, + "scenes": [{"id": "1", "name": "Scene 1"}], + "lights": ["1"], + } +} + +LIGHT = { + "1": { + "id": "Light 1 id", + "name": "Light 1 name", + "state": {"reachable": True}, + "type": "Light", + "uniqueid": "00:00:00:00:00:00:00:01-00", + } +} + +SENSOR = { + "1": { + "id": "Sensor 1 id", + "name": "Sensor 1 name", + "type": "ZHALightLevel", + "state": {"lightlevel": 30000, "dark": False}, + "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + } +} + + +async def setup_deconz_integration(hass, options): + """Create the deCONZ gateway.""" + config_entry = config_entries.ConfigEntry( + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=ENTRY_CONFIG, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, + options=options, + entry_id="1", + ) + + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=DECONZ_WEB_REQUEST + ): + await deconz.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][BRIDGEID] + + +async def test_service_setup(hass): + """Verify service setup works.""" + assert deconz.services.DECONZ_SERVICES not in hass.data + with patch( + "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True) + ) as async_register: + await deconz.services.async_setup_services(hass) + assert hass.data[deconz.services.DECONZ_SERVICES] is True + assert async_register.call_count == 2 + + +async def test_service_setup_already_registered(hass): + """Make sure that services are only registered once.""" + hass.data[deconz.services.DECONZ_SERVICES] = True + with patch( + "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True) + ) as async_register: + await deconz.services.async_setup_services(hass) + async_register.assert_not_called() + + +async def test_service_unload(hass): + """Verify service unload works.""" + hass.data[deconz.services.DECONZ_SERVICES] = True + with patch( + "homeassistant.core.ServiceRegistry.async_remove", return_value=Mock(True) + ) as async_remove: + await deconz.services.async_unload_services(hass) + assert hass.data[deconz.services.DECONZ_SERVICES] is False + assert async_remove.call_count == 2 + + +async def test_service_unload_not_registered(hass): + """Make sure that services can only be unloaded once.""" + with patch( + "homeassistant.core.ServiceRegistry.async_remove", return_value=Mock(True) + ) as async_remove: + await deconz.services.async_unload_services(hass) + assert deconz.services.DECONZ_SERVICES not in hass.data + async_remove.assert_not_called() + + +async def test_configure_service_with_field(hass): + """Test that service invokes pydeconz with the correct path and data.""" + await setup_deconz_integration(hass, options={}) + + data = { + deconz.services.SERVICE_FIELD: "/light/2", + deconz.CONF_BRIDGEID: BRIDGEID, + deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, + } + + with patch( + "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) + ) as put_state: + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + put_state.assert_called_with("/light/2", {"on": True, "attr1": 10, "attr2": 20}) + + +async def test_configure_service_with_entity(hass): + """Test that service invokes pydeconz with the correct path and data.""" + gateway = await setup_deconz_integration(hass, options={}) + + gateway.deconz_ids["light.test"] = "/light/1" + data = { + deconz.services.SERVICE_ENTITY: "light.test", + deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, + } + + with patch( + "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) + ) as put_state: + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + put_state.assert_called_with("/light/1", {"on": True, "attr1": 10, "attr2": 20}) + + +async def test_configure_service_with_entity_and_field(hass): + """Test that service invokes pydeconz with the correct path and data.""" + gateway = await setup_deconz_integration(hass, options={}) + + gateway.deconz_ids["light.test"] = "/light/1" + data = { + deconz.services.SERVICE_ENTITY: "light.test", + deconz.services.SERVICE_FIELD: "/state", + deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, + } + + with patch( + "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) + ) as put_state: + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + put_state.assert_called_with( + "/light/1/state", {"on": True, "attr1": 10, "attr2": 20} + ) + + +async def test_configure_service_with_faulty_field(hass): + """Test that service invokes pydeconz with the correct path and data.""" + await setup_deconz_integration(hass, options={}) + + data = {deconz.services.SERVICE_FIELD: "light/2", deconz.services.SERVICE_DATA: {}} + + with pytest.raises(vol.Invalid): + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + + +async def test_configure_service_with_faulty_entity(hass): + """Test that service invokes pydeconz with the correct path and data.""" + await setup_deconz_integration(hass, options={}) + + data = { + deconz.services.SERVICE_ENTITY: "light.nonexisting", + deconz.services.SERVICE_DATA: {}, + } + + with patch( + "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) + ) as put_state: + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + put_state.assert_not_called() + + +async def test_service_refresh_devices(hass): + """Test that service can refresh devices.""" + gateway = await setup_deconz_integration(hass, options={}) + + data = {deconz.CONF_BRIDGEID: BRIDGEID} + + with patch( + "pydeconz.DeconzSession.async_get_state", + return_value={"groups": GROUP, "lights": LIGHT, "sensors": SENSOR}, + ): + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_DEVICE_REFRESH, service_data=data + ) + await hass.async_block_till_done() + + assert gateway.deconz_ids == { + "light.group_1_name": "/groups/1", + "light.light_1_name": "/lights/1", + "scene.group_1_name_scene_1": "/groups/1/scenes/1", + "sensor.sensor_1_name": "/sensors/1", + } From db48d5effdfc40d1ade8e54ea9fc52801e1e300a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 16 Sep 2019 10:34:31 +0200 Subject: [PATCH 032/296] Update azure-pipelines-ci.yml for Azure Pipelines --- azure-pipelines-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 558c0c39f663b9..13f0915bc56f12 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -112,6 +112,8 @@ stages: # Find offending deps with `pipdeptree -r -p typing` pip uninstall -y typing - script: | + set -e + . venv/bin/activate pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar tests script/check_dirty From 8de84c53a14f42e5ebc9e231cd60616ca4d73197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 16 Sep 2019 12:14:05 +0100 Subject: [PATCH 033/296] zha: fix 0 second transitions being ignored. (#26654) Allow turning a light on instantly, with no transition time. This is actually required for IKEA lights to be able to set brightness and color temp in a single call. --- homeassistant/components/zha/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 27257e5039aee2..c2273c54073ac0 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -201,7 +201,7 @@ def async_restore_last_state(self, last_state): async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) - duration = transition * 10 if transition else DEFAULT_DURATION + duration = transition * 10 if transition is not None else DEFAULT_DURATION brightness = kwargs.get(light.ATTR_BRIGHTNESS) effect = kwargs.get(light.ATTR_EFFECT) From c088e8fd956783c67defbfe2dd6c706fb5c9c446 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Mon, 16 Sep 2019 21:20:48 +0200 Subject: [PATCH 034/296] pytfiac version bump to 0.4 (#26669) --- homeassistant/components/tfiac/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tfiac/manifest.json b/homeassistant/components/tfiac/manifest.json index 9997ae00f0a4f4..d7282317d95dfb 100644 --- a/homeassistant/components/tfiac/manifest.json +++ b/homeassistant/components/tfiac/manifest.json @@ -3,7 +3,7 @@ "name": "Tfiac", "documentation": "https://www.home-assistant.io/components/tfiac", "requirements": [ - "pytfiac==0.3" + "pytfiac==0.4" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 056b51c345774c..049e3a423c24f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1470,7 +1470,7 @@ pytautulli==0.5.0 pyteleloisirs==3.5 # homeassistant.components.tfiac -pytfiac==0.3 +pytfiac==0.4 # homeassistant.components.thinkingcleaner pythinkingcleaner==0.0.3 From 771c674e90845a4eb15f20cbc8608e2f1a29824d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 17 Sep 2019 00:32:14 +0000 Subject: [PATCH 035/296] [ci skip] Translation update --- .../components/adguard/.translations/es.json | 18 +++++++--- .../arcam_fmj/.translations/es.json | 5 +++ .../arcam_fmj/.translations/lb.json | 5 +++ .../components/axis/.translations/es.json | 1 + .../cert_expiry/.translations/es.json | 8 +++-- .../cert_expiry/.translations/sl.json | 2 +- .../components/deconz/.translations/bg.json | 29 +++++++++++++++ .../components/deconz/.translations/es.json | 24 ++++++++++++- .../components/deconz/.translations/lb.json | 29 ++++++++++++++- .../components/deconz/.translations/nl.json | 29 +++++++++++++++ .../components/deconz/.translations/no.json | 36 +++++++++++++++++++ .../components/esphome/.translations/es.json | 1 + .../components/esphome/.translations/lb.json | 2 +- .../geonetnz_quakes/.translations/es.json | 7 ++-- .../components/heos/.translations/lb.json | 2 +- .../homekit_controller/.translations/es.json | 3 +- .../homekit_controller/.translations/lb.json | 2 +- .../homematicip_cloud/.translations/lb.json | 2 +- .../components/hue/.translations/es.json | 2 ++ .../iaqualink/.translations/bg.json | 21 +++++++++++ .../iaqualink/.translations/es.json | 13 +++++-- .../iaqualink/.translations/lb.json | 21 +++++++++++ .../iaqualink/.translations/no.json | 21 +++++++++++ .../components/life360/.translations/es.json | 1 + .../components/light/.translations/bg.json | 13 +++++++ .../components/light/.translations/es.json | 1 + .../components/light/.translations/lb.json | 17 +++++++++ .../components/light/.translations/no.json | 9 +++++ .../components/light/.translations/sl.json | 4 +-- .../components/linky/.translations/bg.json | 25 +++++++++++++ .../components/linky/.translations/es.json | 14 ++++++-- .../components/linky/.translations/lb.json | 22 ++++++++++++ .../components/linky/.translations/no.json | 25 +++++++++++++ .../components/met/.translations/es.json | 5 +-- .../components/met/.translations/lb.json | 2 +- .../components/plaato/.translations/es.json | 3 +- .../components/point/.translations/es.json | 2 +- .../components/ps4/.translations/lb.json | 6 ++-- .../solaredge/.translations/bg.json | 20 +++++++++++ .../solaredge/.translations/es.json | 12 +++++-- .../solaredge/.translations/lb.json | 19 ++++++++++ .../solaredge/.translations/no.json | 21 +++++++++++ .../components/switch/.translations/bg.json | 17 +++++++++ .../components/switch/.translations/es.json | 1 + .../components/switch/.translations/lb.json | 17 +++++++++ .../components/switch/.translations/no.json | 17 +++++++++ .../tellduslive/.translations/es.json | 1 + .../components/traccar/.translations/es.json | 3 +- .../twentemilieu/.translations/es.json | 10 ++++-- .../components/unifi/.translations/ca.json | 6 ++++ .../components/unifi/.translations/es.json | 7 ++++ .../components/velbus/.translations/es.json | 8 +++-- .../components/vesync/.translations/es.json | 6 +++- .../components/vesync/.translations/lb.json | 17 +++++++++ .../components/withings/.translations/lb.json | 12 +++++++ 55 files changed, 586 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/es.json create mode 100644 homeassistant/components/arcam_fmj/.translations/lb.json create mode 100644 homeassistant/components/iaqualink/.translations/bg.json create mode 100644 homeassistant/components/iaqualink/.translations/lb.json create mode 100644 homeassistant/components/iaqualink/.translations/no.json create mode 100644 homeassistant/components/light/.translations/bg.json create mode 100644 homeassistant/components/light/.translations/lb.json create mode 100644 homeassistant/components/linky/.translations/bg.json create mode 100644 homeassistant/components/linky/.translations/lb.json create mode 100644 homeassistant/components/linky/.translations/no.json create mode 100644 homeassistant/components/solaredge/.translations/bg.json create mode 100644 homeassistant/components/solaredge/.translations/lb.json create mode 100644 homeassistant/components/solaredge/.translations/no.json create mode 100644 homeassistant/components/switch/.translations/bg.json create mode 100644 homeassistant/components/switch/.translations/lb.json create mode 100644 homeassistant/components/switch/.translations/no.json create mode 100644 homeassistant/components/vesync/.translations/lb.json create mode 100644 homeassistant/components/withings/.translations/lb.json diff --git a/homeassistant/components/adguard/.translations/es.json b/homeassistant/components/adguard/.translations/es.json index 46f21d96195465..5886d8e5c5b6fc 100644 --- a/homeassistant/components/adguard/.translations/es.json +++ b/homeassistant/components/adguard/.translations/es.json @@ -1,20 +1,30 @@ { "config": { "abort": { - "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente." + "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.", + "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." }, "error": { "connection_error": "No se conect\u00f3." }, "step": { + "hassio_confirm": { + "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Hass.io: {addon} ?", + "title": "AdGuard Home a trav\u00e9s del complemento Hass.io" + }, "user": { "data": { "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Nombre de usuario" - } + "ssl": "AdGuard Home utiliza un certificado SSL", + "username": "Nombre de usuario", + "verify_ssl": "AdGuard Home utiliza un certificado apropiado" + }, + "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control.", + "title": "Enlace su AdGuard Home." } - } + }, + "title": "AdGuard Home" } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/es.json b/homeassistant/components/arcam_fmj/.translations/es.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/es.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/lb.json b/homeassistant/components/arcam_fmj/.translations/lb.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/lb.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/es.json b/homeassistant/components/axis/.translations/es.json index 817737eee04d88..d29481a3be96f8 100644 --- a/homeassistant/components/axis/.translations/es.json +++ b/homeassistant/components/axis/.translations/es.json @@ -8,6 +8,7 @@ }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en curso.", "device_unavailable": "El dispositivo no est\u00e1 disponible", "faulty_credentials": "Credenciales de usuario incorrectas" }, diff --git a/homeassistant/components/cert_expiry/.translations/es.json b/homeassistant/components/cert_expiry/.translations/es.json index 2cb0bd9af166ed..b10518646ac907 100644 --- a/homeassistant/components/cert_expiry/.translations/es.json +++ b/homeassistant/components/cert_expiry/.translations/es.json @@ -5,8 +5,9 @@ }, "error": { "certificate_fetch_failed": "No se puede obtener el certificado de esta combinaci\u00f3n de host y puerto", - "connection_timeout": "Tiempo de espera agotado al conectar con el dispositivo.", - "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" + "connection_timeout": "Tiempo de espera agotado al conectar a este host", + "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", + "resolve_failed": "Este host no se puede resolver" }, "step": { "user": { @@ -14,7 +15,8 @@ "host": "El nombre de host del certificado", "name": "El nombre del certificado", "port": "El puerto del certificado" - } + }, + "title": "Defina el certificado para probar" } }, "title": "Caducidad del certificado" diff --git a/homeassistant/components/cert_expiry/.translations/sl.json b/homeassistant/components/cert_expiry/.translations/sl.json index c088e414c73dbd..3774956330a52a 100644 --- a/homeassistant/components/cert_expiry/.translations/sl.json +++ b/homeassistant/components/cert_expiry/.translations/sl.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Iz te kombinacije gostitelja in vrat ni mogo\u010de pridobiti potrdila", - "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem", + "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem je potekla", "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti" }, diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index a02a6ff422338f..f3eead4aae023a 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -40,5 +40,34 @@ } }, "title": "deCONZ" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430", + "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "close": "\u0417\u0430\u0442\u0432\u0430\u0440\u044f\u043d\u0435", + "dim_down": "\u0417\u0430\u0442\u044a\u043c\u043d\u044f\u0432\u0430\u043d\u0435", + "dim_up": "\u041e\u0441\u0432\u0435\u0442\u044f\u0432\u0430\u043d\u0435", + "left": "\u041b\u044f\u0432\u043e", + "open": "\u041e\u0442\u0432\u0430\u0440\u044f\u043d\u0435", + "right": "\u0414\u044f\u0441\u043d\u043e", + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e", + "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_rotated": "\u0417\u0430\u0432\u044a\u0440\u0442\u044f\u043d \u0431\u0443\u0442\u043e\u043d \"{subtype}\"", + "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", + "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", + "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 3d2b3f17814a49..1bc6c8211a268d 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured": "El puente ya esta configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en curso.", "no_bridges": "No se han descubierto puentes deCONZ", + "not_deconz_bridge": "No es un puente deCONZ", "one_instance_only": "El componente solo admite una instancia de deCONZ", "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, @@ -41,21 +43,41 @@ }, "device_automation": { "trigger_subtype": { + "both_buttons": "Ambos botones", + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", "close": "Cerrar", "dim_down": "Bajar la intensidad", "dim_up": "Subir la intensidad", "left": "Izquierda", + "open": "Abierto", "right": "Derecha", "turn_off": "Apagar", "turn_on": "Encender" + }, + "trigger_type": { + "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces consecutivas", + "remote_button_long_press": "bot\u00f3n \"{subtype}\" pulsado continuamente", + "remote_button_long_release": "Bot\u00f3n \"{subtype}\" liberado despu\u00e9s de un rato pulsado", + "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", + "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", + "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", + "remote_button_short_press": "bot\u00f3n \"{subtype}\" pulsado", + "remote_button_short_release": "bot\u00f3n \"{subtype}\" liberado", + "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", + "remote_gyro_activated": "Dispositivo sacudido" } }, "options": { "step": { "async_step_deconz_devices": { "data": { + "allow_clip_sensor": "Permitir sensores deCONZ CLIP", "allow_deconz_groups": "Permitir grupos de luz deCONZ" - } + }, + "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ" }, "deconz_devices": { "data": { diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 60a27304d78437..41c75ec4aab242 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -23,7 +23,7 @@ "init": { "data": { "host": "Host", - "port": "Port (Standard Wert: '80')" + "port": "Port" }, "title": "deCONZ gateway d\u00e9fin\u00e9ieren" }, @@ -40,5 +40,32 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "B\u00e9id Kn\u00e4ppchen", + "button_1": "\u00c9ischte Kn\u00e4ppchen", + "button_2": "Zweete Kn\u00e4ppchen", + "button_3": "Dr\u00ebtte Kn\u00e4ppchen", + "button_4": "V\u00e9ierte Kn\u00e4ppchen", + "close": "Zoumaachen", + "left": "L\u00e9nks", + "open": "Op", + "right": "Riets", + "turn_off": "Ausschalten", + "turn_on": "Uschalten" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" Kn\u00e4ppche zwee mol gedr\u00e9ckt", + "remote_button_long_press": "\"{subtype}\" Kn\u00e4ppche permanent gedr\u00e9ckt", + "remote_button_long_release": "\"{subtype}\" Kn\u00e4ppche no laangem unhalen lassgelooss", + "remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt", + "remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt", + "remote_button_rotated": "Kn\u00e4ppche gedr\u00e9int \"{subtype}\"", + "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", + "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", + "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", + "remote_gyro_activated": "Apparat ger\u00ebselt" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index f9f2d40488f6a4..116f6254b37213 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee gateway" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Beide knoppen", + "button_1": "Eerste knop", + "button_2": "Tweede knop", + "button_3": "Derde knop", + "button_4": "Vierde knop", + "close": "Sluiten", + "dim_down": "Dim omlaag", + "dim_up": "Dim omhoog", + "left": "Links", + "open": "Open", + "right": "Rechts", + "turn_off": "Uitschakelen", + "turn_on": "Inschakelen" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" knop dubbel geklikt", + "remote_button_long_press": "\" {subtype} \" knop continu ingedrukt", + "remote_button_long_release": "\"{subtype}\" knop losgelaten na lang indrukken van de knop", + "remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt", + "remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt", + "remote_button_rotated": "Knop gedraaid \" {subtype} \"", + "remote_button_short_press": "\" {subtype} \" knop ingedrukt", + "remote_button_short_release": "\"{subtype}\" knop losgelaten", + "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt", + "remote_gyro_activated": "Apparaat geschud" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 8798248224a582..7a93c6ff9cf850 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee gateway" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Begge knappene", + "button_1": "F\u00f8rste knapp", + "button_2": "Andre knapp", + "button_3": "Tredje knapp", + "button_4": "Fjerde knapp", + "close": "Lukk", + "dim_down": "Dimm ned", + "dim_up": "Dimm opp", + "left": "Venstre", + "open": "\u00c5pen", + "right": "H\u00f8yre", + "turn_off": "Skru av", + "turn_on": "Sl\u00e5 p\u00e5" + }, + "trigger_type": { + "remote_button_double_press": "\"{under type}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{undertype}\" - knappen ble kontinuerlig trykket", + "remote_button_long_release": "\" {subtype} \" -knapp sluppet etter langt trykk", + "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{undertype}\" - knappen femdobbelt klikket", + "remote_button_rotated": "Knappen roterte \" {subtype} \"", + "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", + "remote_button_short_release": "\" {subtype} \" -knappen ble utgitt", + "remote_button_triple_press": "\"{under type}\"-knappen trippel klikket", + "remote_gyro_activated": "Enhet er ristet" + } + }, "options": { "step": { "async_step_deconz_devices": { @@ -49,6 +78,13 @@ "allow_deconz_groups": "Tillat deCONZ lys grupper" }, "description": "Konfigurere synlighet av deCONZ enhetstyper" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Tillat deCONZ CLIP-sensorer", + "allow_deconz_groups": "Tillat deCONZ lys grupper" + }, + "description": "Konfigurere synlighet av deCONZ enhetstyper" } } } diff --git a/homeassistant/components/esphome/.translations/es.json b/homeassistant/components/esphome/.translations/es.json index 88730a18554e9a..70d766cf4c0544 100644 --- a/homeassistant/components/esphome/.translations/es.json +++ b/homeassistant/components/esphome/.translations/es.json @@ -8,6 +8,7 @@ "invalid_password": "\u00a1Contrase\u00f1a incorrecta!", "resolve_error": "No se puede resolver la direcci\u00f3n de ESP. Si el error persiste, configura una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "Desplom\u00e9: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/.translations/lb.json b/homeassistant/components/esphome/.translations/lb.json index 955b050bc5b19d..882b67823ba288 100644 --- a/homeassistant/components/esphome/.translations/lb.json +++ b/homeassistant/components/esphome/.translations/lb.json @@ -14,7 +14,7 @@ "data": { "password": "Passwuert" }, - "description": "Gitt d'Passwuert vun \u00e4rer Konfiguratioun an.", + "description": "Gitt d'Passwuert vun \u00e4rer Konfiguratioun an fir {name}.", "title": "Passwuert aginn" }, "discovery_confirm": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/es.json b/homeassistant/components/geonetnz_quakes/.translations/es.json index 41404822dd8289..f6f592675ab33e 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/es.json +++ b/homeassistant/components/geonetnz_quakes/.translations/es.json @@ -6,9 +6,12 @@ "step": { "user": { "data": { + "mmi": "MMI", "radius": "Radio" - } + }, + "title": "Complete todos los campos requeridos" } - } + }, + "title": "GeoNet NZ Quakes" } } \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/lb.json b/homeassistant/components/heos/.translations/lb.json index 416f0878de46a3..cfe1d347b0cc20 100644 --- a/homeassistant/components/heos/.translations/lb.json +++ b/homeassistant/components/heos/.translations/lb.json @@ -16,6 +16,6 @@ "title": "Mat Heos verbannen" } }, - "title": "Heos" + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/es.json b/homeassistant/components/homekit_controller/.translations/es.json index 642e76fd1dd0d3..67f6daa8469d8e 100644 --- a/homeassistant/components/homekit_controller/.translations/es.json +++ b/homeassistant/components/homekit_controller/.translations/es.json @@ -3,6 +3,7 @@ "abort": { "accessory_not_found_error": "No se puede a\u00f1adir el emparejamiento porque ya no se puede encontrar el dispositivo.", "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", + "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en curso.", "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicia el accesorio e int\u00e9ntalo de nuevo.", "ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.", "invalid_config_entry": "Este dispositivo se muestra como listo para vincular, pero ya existe una entrada que causa conflicto en Home Assistant y se debe eliminar primero.", @@ -23,7 +24,7 @@ "data": { "pairing_code": "C\u00f3digo de vinculaci\u00f3n" }, - "description": "Introduce tu c\u00f3digo de vinculaci\u00f3n de HomeKit para usar este accesorio", + "description": "Introduce tu c\u00f3digo de vinculaci\u00f3n de HomeKit (en este formato XXX-XX-XXX) para usar este accesorio", "title": "Vincular con accesorio HomeKit" }, "user": { diff --git a/homeassistant/components/homekit_controller/.translations/lb.json b/homeassistant/components/homekit_controller/.translations/lb.json index 97efd428a0469e..ca7bce44508243 100644 --- a/homeassistant/components/homekit_controller/.translations/lb.json +++ b/homeassistant/components/homekit_controller/.translations/lb.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "Pairing Code" }, - "description": "Gitt \u00e4ren HomeKit pairing Code an fir d\u00ebsen Accessoire ze benotzen", + "description": "Gitt \u00e4ren HomeKit pairing Code (am Format XXX-XX-XXX) an fir d\u00ebsen Accessoire ze benotzen", "title": "Mam HomeKit Accessoire verbannen" }, "user": { diff --git a/homeassistant/components/homematicip_cloud/.translations/lb.json b/homeassistant/components/homematicip_cloud/.translations/lb.json index 2cad909a7ee54e..f8ae990d36442a 100644 --- a/homeassistant/components/homematicip_cloud/.translations/lb.json +++ b/homeassistant/components/homematicip_cloud/.translations/lb.json @@ -21,7 +21,7 @@ "title": "HomematicIP Accesspoint auswielen" }, "link": { - "description": "Dr\u00e9ckt de bloen Kn\u00e4ppchen um Accesspoint an den Submit Kn\u00e4ppchen fir d'HomematicIP mam Home Assistant ze registr\u00e9ieren.", + "description": "Dr\u00e9ckt de bloen Kn\u00e4ppchen um Accesspoint an den Submit Kn\u00e4ppchen fir d'HomematicIP mam Home Assistant ze registr\u00e9ieren.\n\n![Standuert vum Kn\u00e4ppchen op der Bridge](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Accesspoint verbannen" } }, diff --git a/homeassistant/components/hue/.translations/es.json b/homeassistant/components/hue/.translations/es.json index 56e7ed62e9dc8a..3ec9ed871d3dca 100644 --- a/homeassistant/components/hue/.translations/es.json +++ b/homeassistant/components/hue/.translations/es.json @@ -3,9 +3,11 @@ "abort": { "all_configured": "Todos los puentes Philips Hue ya est\u00e1n configurados", "already_configured": "El puente ya esta configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en curso.", "cannot_connect": "No se puede conectar al puente", "discover_timeout": "No se han descubierto puentes Philips Hue", "no_bridges": "No se han descubierto puentes Philips Hue.", + "not_hue_bridge": "No es un puente Hue", "unknown": "Se produjo un error desconocido" }, "error": { diff --git a/homeassistant/components/iaqualink/.translations/bg.json b/homeassistant/components/iaqualink/.translations/bg.json new file mode 100644 index 00000000000000..5b37bde3ee3a81 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\u041c\u043e\u0436\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 iAqualink." + }, + "error": { + "connection_failure": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 iAqualink. \u041f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 / \u0438\u043c\u0435\u0439\u043b \u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u041c\u043e\u043b\u044f \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f iAqualink \u0430\u043a\u0430\u0443\u043d\u0442.", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/es.json b/homeassistant/components/iaqualink/.translations/es.json index 7326d80497be4a..698be68bd78e9c 100644 --- a/homeassistant/components/iaqualink/.translations/es.json +++ b/homeassistant/components/iaqualink/.translations/es.json @@ -1,12 +1,21 @@ { "config": { + "abort": { + "already_setup": "Solo puede configurar una \u00fanica conexi\u00f3n iAqualink." + }, + "error": { + "connection_failure": "No se puede conectar a iAqualink. Verifica tu nombre de usuario y contrase\u00f1a." + }, "step": { "user": { "data": { "password": "Contrase\u00f1a", "username": "Usuario / correo electr\u00f3nico" - } + }, + "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a de su cuenta de iAqualink.", + "title": "Con\u00e9ctese a iAqualink" } - } + }, + "title": "Jandy iAqualink" } } \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/lb.json b/homeassistant/components/iaqualink/.translations/lb.json new file mode 100644 index 00000000000000..4beb11214bc2c8 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen eng eenzeg iAqualink Verbindung konfigur\u00e9ieren." + }, + "error": { + "connection_failure": "Kann sech net mat iAqualink verbannen. Iwwerpr\u00e9ift \u00e4ren Benotzernumm an Passwuert" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm / E-Mail Adresse" + }, + "description": "Gitt den Benotznumm an d'Passwuert fir \u00e4ren iAqualink Kont un.", + "title": "Mat iAqualink verbannen" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/no.json b/homeassistant/components/iaqualink/.translations/no.json new file mode 100644 index 00000000000000..9d464a6d516c55 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bare konfigurere en enkel iAqualink-tilkobling." + }, + "error": { + "connection_failure": "Kan ikke koble til iAqualink. Sjekk brukernavnet og passordet ditt." + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn / E-postadresse" + }, + "description": "Vennligst skriv inn brukernavn og passord for iAqualink-kontoen din.", + "title": "Koble til iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/es.json b/homeassistant/components/life360/.translations/es.json index 28999de5e81b94..2b185cb1b6c476 100644 --- a/homeassistant/components/life360/.translations/es.json +++ b/homeassistant/components/life360/.translations/es.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Credenciales no v\u00e1lidas", "invalid_username": "Nombre de usuario no v\u00e1lido", + "unexpected": "Error inesperado al comunicarse con el servidor Life360", "user_already_configured": "La cuenta ya ha sido configurada" }, "step": { diff --git a/homeassistant/components/light/.translations/bg.json b/homeassistant/components/light/.translations/bg.json new file mode 100644 index 00000000000000..533ba76b6a7613 --- /dev/null +++ b/homeassistant/components/light/.translations/bg.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u041f\u0440\u0435\u0432\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b.", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/es.json b/homeassistant/components/light/.translations/es.json index 93dfc65bbe1c0a..fcbe835e4c2a9b 100644 --- a/homeassistant/components/light/.translations/es.json +++ b/homeassistant/components/light/.translations/es.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Alternar {entity_name}", "turn_off": "Apagar {entity_name}", "turn_on": "Encender {entity_name}" }, diff --git a/homeassistant/components/light/.translations/lb.json b/homeassistant/components/light/.translations/lb.json new file mode 100644 index 00000000000000..fdd76cda7e62fc --- /dev/null +++ b/homeassistant/components/light/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} \u00ebmschalten", + "turn_off": "{entity_name} ausschalten", + "turn_on": "{entity_name} uschalten" + }, + "condition_type": { + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un" + }, + "trigger_type": { + "turn_off": "{entity_name} gouf ausgeschalt", + "turn_on": "{entity_name} gouf ugeschalt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/no.json b/homeassistant/components/light/.translations/no.json index 39c391eff3356a..2241ca6644e9c0 100644 --- a/homeassistant/components/light/.translations/no.json +++ b/homeassistant/components/light/.translations/no.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Veksle {entity_name}", + "turn_off": "Sl\u00e5 av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er av", + "is_on": "{entity_name} er p\u00e5" + }, "trigger_type": { "turn_off": "{name} sl\u00e5tt av", "turn_on": "{name} sl\u00e5tt p\u00e5" diff --git a/homeassistant/components/light/.translations/sl.json b/homeassistant/components/light/.translations/sl.json index afd59d619e0cdc..432c8ae37d15fb 100644 --- a/homeassistant/components/light/.translations/sl.json +++ b/homeassistant/components/light/.translations/sl.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} je vklopljen" }, "trigger_type": { - "turn_off": "{name} izklopljeno", - "turn_on": "{name} vklopljeno" + "turn_off": "{entity_name} izklopljen", + "turn_on": "{entity_name} vklopljen" } } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/bg.json b/homeassistant/components/linky/.translations/bg.json new file mode 100644 index 00000000000000..6eeb898ee1ffc8 --- /dev/null +++ b/homeassistant/components/linky/.translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b" + }, + "error": { + "access": "\u041d\u044f\u043c\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e Enedis.fr, \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0438", + "enedis": "Enedis.fr \u043e\u0442\u0433\u043e\u0432\u043e\u0440\u0438 \u0441 \u0433\u0440\u0435\u0448\u043a\u0430: \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u043d\u0435 \u043c\u0435\u0436\u0434\u0443 23:00 \u0438 02:00)", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430: \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u043d\u0435 \u043c\u0435\u0436\u0434\u0443 23:00 \u0438 02:00)", + "username_exists": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b", + "wrong_login": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0432\u043b\u0438\u0437\u0430\u043d\u0435: \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0438\u043c\u0435\u0439\u043b\u0430 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "E-mail" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043d\u0434\u0435\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u0442\u0435 \u0441\u0438 \u0434\u0430\u043d\u043d\u0438", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/es.json b/homeassistant/components/linky/.translations/es.json index 7c0d17c8a8f1e6..511f3c9d8e56f8 100644 --- a/homeassistant/components/linky/.translations/es.json +++ b/homeassistant/components/linky/.translations/es.json @@ -4,12 +4,22 @@ "username_exists": "Cuenta ya configurada" }, "error": { + "access": "No se pudo acceder a Enedis.fr, compruebe su conexi\u00f3n a Internet", + "enedis": "Enedis.fr respondi\u00f3 con un error: vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11:00 y las 2 de la ma\u00f1ana)", + "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 23:00 y las 02:00 horas).", + "username_exists": "Cuenta ya configurada", "wrong_login": "Error de inicio de sesi\u00f3n: compruebe su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a" }, "step": { "user": { - "description": "Introduzca sus credenciales" + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + }, + "description": "Introduzca sus credenciales", + "title": "Linky" } - } + }, + "title": "Linky" } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/lb.json b/homeassistant/components/linky/.translations/lb.json new file mode 100644 index 00000000000000..d38002385590fa --- /dev/null +++ b/homeassistant/components/linky/.translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "username_exists": "Kont ass scho konfigur\u00e9iert" + }, + "error": { + "username_exists": "Kont ass scho konfigur\u00e9iert", + "wrong_login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail" + }, + "description": "F\u00ebllt \u00e4r Login Informatiounen aus", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/no.json b/homeassistant/components/linky/.translations/no.json new file mode 100644 index 00000000000000..c43f434562c152 --- /dev/null +++ b/homeassistant/components/linky/.translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Kontoen er allerede konfigurert" + }, + "error": { + "access": "Kunne ikke f\u00e5 tilgang til Enedis.fr, vennligst sjekk internettforbindelsen din", + "enedis": "Enedis.fr svarte med en feil: vennligst pr\u00f8v p\u00e5 nytt senere (vanligvis ikke mellom 23:00 og 02:00)", + "unknown": "Ukjent feil: pr\u00f8v p\u00e5 nytt senere (vanligvis ikke mellom 23:00 og 02:00)", + "username_exists": "Kontoen er allerede konfigurert", + "wrong_login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-post" + }, + "description": "Skriv inn legitimasjonen din", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/es.json b/homeassistant/components/met/.translations/es.json index 7659ab4d2962a8..a475518bd851cb 100644 --- a/homeassistant/components/met/.translations/es.json +++ b/homeassistant/components/met/.translations/es.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "El nombre ya existe" + "name_exists": "La ubicaci\u00f3n ya existe" }, "step": { "user": { @@ -14,6 +14,7 @@ "description": "Instituto de meteorolog\u00eda", "title": "Ubicaci\u00f3n" } - } + }, + "title": "Met.no" } } \ No newline at end of file diff --git a/homeassistant/components/met/.translations/lb.json b/homeassistant/components/met/.translations/lb.json index 660f639d859718..9f91d37c23360c 100644 --- a/homeassistant/components/met/.translations/lb.json +++ b/homeassistant/components/met/.translations/lb.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Numm g\u00ebtt et schonn" + "name_exists": "Standuert g\u00ebtt et schonn" }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/es.json b/homeassistant/components/plaato/.translations/es.json index e52a80be986370..ecb061e91c9c44 100644 --- a/homeassistant/components/plaato/.translations/es.json +++ b/homeassistant/components/plaato/.translations/es.json @@ -12,6 +12,7 @@ "description": "\u00bfEst\u00e1s seguro de que quieres configurar el Airlock de Plaato?", "title": "Configurar el webhook de Plaato" } - } + }, + "title": "Plaato Airlock" } } \ No newline at end of file diff --git a/homeassistant/components/point/.translations/es.json b/homeassistant/components/point/.translations/es.json index 33b6b1d38271a9..9a94e54dd5fa30 100644 --- a/homeassistant/components/point/.translations/es.json +++ b/homeassistant/components/point/.translations/es.json @@ -27,6 +27,6 @@ "title": "Proveedor de autenticaci\u00f3n" } }, - "title": "Point de Minut" + "title": "Minut Point" } } \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/lb.json b/homeassistant/components/ps4/.translations/lb.json index 17757cb9d20317..0986b0e0240afb 100644 --- a/homeassistant/components/ps4/.translations/lb.json +++ b/homeassistant/components/ps4/.translations/lb.json @@ -4,8 +4,8 @@ "credential_error": "Feeler beim ausliesen vun den Umeldungs Informatiounen.", "devices_configured": "All Apparater sinn schonn konfigur\u00e9iert", "no_devices_found": "Keng Playstation 4 am Netzwierk fonnt.", - "port_987_bind_error": "Konnt sech net mam Port 987 verbannen.", - "port_997_bind_error": "Konnt sech net mam Port 997 verbannen." + "port_987_bind_error": "Konnt sech net mam Port 987 verbannen. Liest [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen.", + "port_997_bind_error": "Konnt sech net mam Port 997 verbannen. Liest [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen." }, "error": { "credential_timeout": "Z\u00e4it Iwwerschreidung beim Service vun den Umeldungsinformatiounen. Dr\u00e9ck op ofsch\u00e9cke fir nach emol ze starten.", @@ -25,7 +25,7 @@ "name": "Numm", "region": "Regioun" }, - "description": "Gitt \u00e4r Playstation 4 Informatiounen an. Fir 'PIN', gitt an d'Astellunge vun der Playstation 4 Konsole. Dann op 'Mobile App Verbindungs Astellungen' a wielt \"Apparat dob\u00e4isetzen' aus. Gitt de PIN an deen ugewise g\u00ebtt.", + "description": "Gitt \u00e4r Playstation 4 Informatiounen an. Fir 'PIN', gitt an d'Astellunge vun der Playstation 4 Konsole. Dann op 'Mobile App Verbindungs Astellungen' a wielt \"Apparat dob\u00e4isetzen' aus. Gitt de PIN an deen ugewise g\u00ebtt. Liest [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/solaredge/.translations/bg.json b/homeassistant/components/solaredge/.translations/bg.json new file mode 100644 index 00000000000000..72f1ad2a4c758f --- /dev/null +++ b/homeassistant/components/solaredge/.translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "site_exists": "\u0422\u043e\u0432\u0430 site_id \u0432\u0435\u0447\u0435 \u0435 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u043e" + }, + "error": { + "site_exists": "\u0422\u043e\u0432\u0430 site_id \u0432\u0435\u0447\u0435 \u0435 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u043e" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447\u0430 \u0437\u0430 \u0442\u043e\u0437\u0438 \u0441\u0430\u0439\u0442", + "name": "\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435\u0442\u043e \u043d\u0430 \u0442\u0430\u0437\u0438 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f", + "site_id": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u044a\u0440\u044a\u0442 site-id \u043d\u0430 SolarEdge" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 (API) \u0437\u0430 \u0442\u0430\u0437\u0438 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/es.json b/homeassistant/components/solaredge/.translations/es.json index 9f52511a165cff..8708729bf4aebd 100644 --- a/homeassistant/components/solaredge/.translations/es.json +++ b/homeassistant/components/solaredge/.translations/es.json @@ -1,13 +1,21 @@ { "config": { + "abort": { + "site_exists": "Este site_id ya est\u00e1 configurado" + }, + "error": { + "site_exists": "Este site_id ya est\u00e1 configurado" + }, "step": { "user": { "data": { "api_key": "La clave de la API para este sitio", - "name": "El nombre de esta instalaci\u00f3n" + "name": "El nombre de esta instalaci\u00f3n", + "site_id": "La identificaci\u00f3n del sitio de SolarEdge" }, "title": "Definir los par\u00e1metros de la API para esta instalaci\u00f3n" } - } + }, + "title": "SolarEdge" } } \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/lb.json b/homeassistant/components/solaredge/.translations/lb.json new file mode 100644 index 00000000000000..957a0187c1d6de --- /dev/null +++ b/homeassistant/components/solaredge/.translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert" + }, + "error": { + "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert" + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel fir d\u00ebsen Site", + "name": "Numm vun d\u00ebser Installatioun" + } + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/no.json b/homeassistant/components/solaredge/.translations/no.json new file mode 100644 index 00000000000000..ad7cb55316b696 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Denne site_id er allerede konfigurert" + }, + "error": { + "site_exists": "Denne site_id er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkelen for dette nettstedet", + "name": "Navnet p\u00e5 denne installasjonen", + "site_id": "SolarEdge nettsted-id" + }, + "title": "Definer API-parametrene for denne installasjonen" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/bg.json b/homeassistant/components/switch/.translations/bg.json new file mode 100644 index 00000000000000..31e41d3f504dea --- /dev/null +++ b/homeassistant/components/switch/.translations/bg.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u041f\u0440\u0435\u0432\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" + }, + "condition_type": { + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" + }, + "trigger_type": { + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/es.json b/homeassistant/components/switch/.translations/es.json index 6749eab129331a..987d13a39403e8 100644 --- a/homeassistant/components/switch/.translations/es.json +++ b/homeassistant/components/switch/.translations/es.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Alternar {entity_name}", "turn_off": "Apagar {entity_name}", "turn_on": "Encender {entity_name}" }, diff --git a/homeassistant/components/switch/.translations/lb.json b/homeassistant/components/switch/.translations/lb.json new file mode 100644 index 00000000000000..291d8cb478f251 --- /dev/null +++ b/homeassistant/components/switch/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} \u00ebmschalten", + "turn_off": "{entity_name} ausschalten", + "turn_on": "{entity_name} uschalten" + }, + "condition_type": { + "turn_off": "{entity_name} gouf ausgeschalt", + "turn_on": "{entity_name} gouf ugeschalt" + }, + "trigger_type": { + "turn_off": "{entity_name} gouf ausgeschalt", + "turn_on": "{entity_name} gouf ugeschalt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/no.json b/homeassistant/components/switch/.translations/no.json new file mode 100644 index 00000000000000..8a00ac09549771 --- /dev/null +++ b/homeassistant/components/switch/.translations/no.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Veksle {entity_name}", + "turn_off": "Sl\u00e5 av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} sl\u00e5tt av", + "turn_on": "{entity_name} sl\u00e5tt p\u00e5" + }, + "trigger_type": { + "turn_off": "{entity_name} sl\u00e5tt av", + "turn_on": "{entity_name} sl\u00e5tt p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/es.json b/homeassistant/components/tellduslive/.translations/es.json index b0313a1eee357a..677e0389d45b71 100644 --- a/homeassistant/components/tellduslive/.translations/es.json +++ b/homeassistant/components/tellduslive/.translations/es.json @@ -18,6 +18,7 @@ "data": { "host": "Host" }, + "description": "Vac\u00edo", "title": "Elige el punto final." } }, diff --git a/homeassistant/components/traccar/.translations/es.json b/homeassistant/components/traccar/.translations/es.json index b0b65a10c83cc9..dedaf02971c79f 100644 --- a/homeassistant/components/traccar/.translations/es.json +++ b/homeassistant/components/traccar/.translations/es.json @@ -12,6 +12,7 @@ "description": "\u00bfEst\u00e1 seguro de querer configurar Traccar?", "title": "Configurar Traccar" } - } + }, + "title": "Traccar" } } \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/es.json b/homeassistant/components/twentemilieu/.translations/es.json index 902e28b2080d87..60a412684f775b 100644 --- a/homeassistant/components/twentemilieu/.translations/es.json +++ b/homeassistant/components/twentemilieu/.translations/es.json @@ -4,7 +4,8 @@ "address_exists": "Direcci\u00f3n ya configurada." }, "error": { - "connection_error": "No se conect\u00f3." + "connection_error": "No se conect\u00f3.", + "invalid_address": "Direcci\u00f3n no encontrada en el \u00e1rea de servicio de Twente Milieu." }, "step": { "user": { @@ -12,8 +13,11 @@ "house_letter": "Letra de la casa/adicional", "house_number": "N\u00famero de casa", "post_code": "C\u00f3digo postal" - } + }, + "description": "Configure Twente Milieu proporcionando informaci\u00f3n sobre la recolecci\u00f3n de residuos en su direcci\u00f3n.", + "title": "Twente Milieu" } - } + }, + "title": "Twente Milieu" } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 8a8d8b11f57661..3741b035d7a9d6 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -32,6 +32,12 @@ "track_devices": "Segueix dispositius de la xarxa (dispositius Ubiquiti)", "track_wired_clients": "Inclou clients de xarxa per cable" } + }, + "init": { + "data": { + "one": "un", + "other": "altre" + } } } } diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 8b0eb56203700d..0539f5607b4c36 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -29,8 +29,15 @@ "data": { "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta considerarlo desconectado", "track_clients": "Seguimiento de los clientes de red", + "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de red cableada" } + }, + "init": { + "data": { + "one": "uno", + "other": "otro" + } } } } diff --git a/homeassistant/components/velbus/.translations/es.json b/homeassistant/components/velbus/.translations/es.json index 1acaaa53ab2f11..1e1e8897c30155 100644 --- a/homeassistant/components/velbus/.translations/es.json +++ b/homeassistant/components/velbus/.translations/es.json @@ -4,14 +4,18 @@ "port_exists": "Este puerto ya est\u00e1 configurado" }, "error": { + "connection_failed": "La conexi\u00f3n velbus fall\u00f3", "port_exists": "Este puerto ya est\u00e1 configurado" }, "step": { "user": { "data": { + "name": "El nombre de esta conexi\u00f3n velbus", "port": "Cadena de conexi\u00f3n" - } + }, + "title": "Definir el tipo de conexi\u00f3n velbus" } - } + }, + "title": "Interfaz Velbus" } } \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/es.json b/homeassistant/components/vesync/.translations/es.json index 99611c5f9bfa79..856dc77a52c48d 100644 --- a/homeassistant/components/vesync/.translations/es.json +++ b/homeassistant/components/vesync/.translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_setup": "Solo se permite una instancia de Vesync" + }, "error": { "invalid_login": "Nombre de usuario o contrase\u00f1a no v\u00e1lidos" }, @@ -11,6 +14,7 @@ }, "title": "Introduzca el nombre de usuario y la contrase\u00f1a" } - } + }, + "title": "VeSync" } } \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/lb.json b/homeassistant/components/vesync/.translations/lb.json new file mode 100644 index 00000000000000..7d1dbad19f3bd8 --- /dev/null +++ b/homeassistant/components/vesync/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_login": "Ong\u00ebltege Benotzernumm oder Passwuert" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail Adresse" + }, + "title": "Benotznumm a Passwuert aginn" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json new file mode 100644 index 00000000000000..994d02aa7f5936 --- /dev/null +++ b/homeassistant/components/withings/.translations/lb.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "title": "Benotzer Profil." + } + } + } +} \ No newline at end of file From 0ef79da281ff0ad48a69b600a39920d13f8a36c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Sep 2019 01:23:31 -0600 Subject: [PATCH 036/296] Use Nabu Casa url if no https url set (#26682) * Use Nabu Casa url if no https url set * Update test_home_assistant_cast.py --- .../components/cast/home_assistant_cast.py | 12 +++++++++- .../cast/test_home_assistant_cast.py | 24 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index f604594bfc5887..d5d35ba7c9f933 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -40,10 +40,20 @@ async def async_setup_ha_cast( async def handle_show_view(call: core.ServiceCall): """Handle a Show View service call.""" + hass_url = hass.config.api.base_url + + # Home Assistant Cast only works with https urls. If user has no configured + # base url, use their remote url. + if not hass_url.lower().startswith("https://"): + try: + hass_url = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass + controller = HomeAssistantController( # If you are developing Home Assistant Cast, uncomment and set to your dev app id. # app_id="5FE44367", - hass_url=hass.config.api.base_url, + hass_url=hass_url, client_id=None, refresh_token=refresh_token.token, ) diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index e67b8f70160472..8db6fd4609e850 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,5 +1,5 @@ """Test Home Assistant Cast.""" -from unittest.mock import Mock +from unittest.mock import Mock, patch from homeassistant.components.cast import home_assistant_cast from tests.common import MockConfigEntry, async_mock_signal @@ -26,3 +26,25 @@ async def test_service_show_view(hass): assert controller.supporting_app_id == "B12CE3CA" assert entity_id == "media_player.kitchen" assert view_path == "mock_path" + + +async def test_use_cloud_url(hass): + """Test that we fall back to cloud url.""" + hass.config.api = Mock(base_url="http://example.com") + await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry()) + calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) + + with patch( + "homeassistant.components.cloud.async_remote_ui_url", + return_value="https://something.nabu.acas", + ): + await hass.services.async_call( + "cast", + "show_lovelace_view", + {"entity_id": "media_player.kitchen", "view_path": "mock_path"}, + blocking=True, + ) + + assert len(calls) == 1 + controller = calls[0][0] + assert controller.hass_url == "https://something.nabu.acas" From e0f1677296e6b7f06e393b4d341bc247de169d53 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 17 Sep 2019 09:34:34 +0200 Subject: [PATCH 037/296] Updated frontend to 20190917.0 (#26686) --- homeassistant/components/frontend/manifest.json | 4 +++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 7052ebfc15ad2e..955c4b90cc6440 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", - "requirements": ["home-assistant-frontend==20190911.1"], + "requirements": [ + "home-assistant-frontend==20190917.0" + ], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d635d178940e3e..b01efba8f0466e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190911.1 +home-assistant-frontend==20190917.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 049e3a423c24f7..f59f0ba19ab185 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190911.1 +home-assistant-frontend==20190917.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7125d3e9ed5a02..297ed6bb866a46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190911.1 +home-assistant-frontend==20190917.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 4be0c057d29d0750208a0716b0e2add9190ff20d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 17 Sep 2019 11:44:43 +0200 Subject: [PATCH 038/296] Fix Nuki issues (#26689) * Fix Nuki issues * remove stale code * Add comments * Fix lint --- CODEOWNERS | 2 +- homeassistant/components/nuki/__init__.py | 2 + homeassistant/components/nuki/lock.py | 90 +++++++-------------- homeassistant/components/nuki/manifest.json | 8 +- 4 files changed, 35 insertions(+), 67 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index fe5e19f9115f37..c454514912ca51 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -195,7 +195,7 @@ homeassistant/components/notify/* @home-assistant/core homeassistant/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte -homeassistant/components/nuki/* @pschmitt +homeassistant/components/nuki/* @pvizeli homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 2e15ac8a68d052..c8b19082585c12 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1 +1,3 @@ """The nuki component.""" + +DOMAIN = "nuki" diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index dc0ae1f22495db..7fda26b290041d 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -1,20 +1,18 @@ """Nuki.io lock platform.""" from datetime import timedelta import logging -import requests +from pynuki import NukiBridge +from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.components.lock import ( - DOMAIN, - PLATFORM_SCHEMA, - LockDevice, - SUPPORT_OPEN, -) +from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockDevice from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import extract_entity_ids +from . import DOMAIN + _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 8080 @@ -30,7 +28,8 @@ NUKI_DATA = "nuki" SERVICE_LOCK_N_GO = "lock_n_go" -SERVICE_CHECK_CONNECTION = "check_connection" + +ERROR_STATES = (0, 254, 255) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -47,48 +46,30 @@ } ) -CHECK_CONNECTION_SERVICE_SCHEMA = vol.Schema( - {vol.Optional(ATTR_ENTITY_ID): cv.entity_ids} -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nuki lock platform.""" - from pynuki import NukiBridge - bridge = NukiBridge( config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], DEFAULT_TIMEOUT ) - add_entities([NukiLock(lock) for lock in bridge.locks]) + devices = [NukiLock(lock) for lock in bridge.locks] def service_handler(service): """Service handler for nuki services.""" entity_ids = extract_entity_ids(hass, service) - all_locks = hass.data[NUKI_DATA][DOMAIN] - target_locks = [] - if not entity_ids: - target_locks = all_locks - else: - for lock in all_locks: - if lock.entity_id in entity_ids: - target_locks.append(lock) - for lock in target_locks: - if service.service == SERVICE_LOCK_N_GO: - unlatch = service.data[ATTR_UNLATCH] - lock.lock_n_go(unlatch=unlatch) - elif service.service == SERVICE_CHECK_CONNECTION: - lock.check_connection() + unlatch = service.data[ATTR_UNLATCH] + + for lock in devices: + if lock.entity_id not in entity_ids: + continue + lock.lock_n_go(unlatch=unlatch) hass.services.register( - "nuki", SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA - ) - hass.services.register( - "nuki", - SERVICE_CHECK_CONNECTION, - service_handler, - schema=CHECK_CONNECTION_SERVICE_SCHEMA, + DOMAIN, SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA ) + add_entities(devices) + class NukiLock(LockDevice): """Representation of a Nuki lock.""" @@ -99,15 +80,7 @@ def __init__(self, nuki_lock): self._locked = nuki_lock.is_locked self._name = nuki_lock.name self._battery_critical = nuki_lock.battery_critical - self._available = nuki_lock.state != 255 - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - if NUKI_DATA not in self.hass.data: - self.hass.data[NUKI_DATA] = {} - if DOMAIN not in self.hass.data[NUKI_DATA]: - self.hass.data[NUKI_DATA][DOMAIN] = [] - self.hass.data[NUKI_DATA][DOMAIN].append(self) + self._available = nuki_lock.state not in ERROR_STATES @property def name(self): @@ -140,13 +113,19 @@ def available(self) -> bool: def update(self): """Update the nuki lock properties.""" - try: - self._nuki_lock.update(aggressive=False) - except requests.exceptions.RequestException: - self._available = False - return + for level in (False, True): + try: + self._nuki_lock.update(aggressive=level) + except RequestException: + _LOGGER.warning("Network issues detect with %s", self.name) + self._available = False + return + + # If in error state, we force an update and repoll data + self._available = self._nuki_lock.state not in ERROR_STATES + if self._available: + break - self._available = True self._name = self._nuki_lock.name self._locked = self._nuki_lock.is_locked self._battery_critical = self._nuki_lock.battery_critical @@ -170,12 +149,3 @@ def lock_n_go(self, unlatch=False, **kwargs): amount of time depending on the lock settings) and relock. """ self._nuki_lock.lock_n_go(unlatch, kwargs) - - def check_connection(self, **kwargs): - """Update the nuki lock properties.""" - try: - self._nuki_lock.update(aggressive=True) - except requests.exceptions.RequestException: - self._available = False - else: - self._available = self._nuki_lock.state != 255 diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 932b80690c4d2c..e7f078a1a0594a 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -2,11 +2,7 @@ "domain": "nuki", "name": "Nuki", "documentation": "https://www.home-assistant.io/components/nuki", - "requirements": [ - "pynuki==1.3.3" - ], + "requirements": ["pynuki==1.3.3"], "dependencies": [], - "codeowners": [ - "@pschmitt" - ] + "codeowners": ["@pvizeli"] } From b7f7d545d173759ac2c1282143847621c418ee58 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 17 Sep 2019 13:48:01 +0200 Subject: [PATCH 039/296] Bump connect-box library to fix logging (#26690) --- homeassistant/components/upc_connect/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 53bd7fc5820a5b..efa38286e7e2f6 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "upc_connect", "name": "Upc connect", "documentation": "https://www.home-assistant.io/components/upc_connect", - "requirements": ["connect-box==0.2.3"], + "requirements": ["connect-box==0.2.4"], "dependencies": [], "codeowners": ["@pvizeli"] } diff --git a/requirements_all.txt b/requirements_all.txt index f59f0ba19ab185..0e432bff8bb7be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -355,7 +355,7 @@ colorlog==4.0.2 concord232==0.15 # homeassistant.components.upc_connect -connect-box==0.2.3 +connect-box==0.2.4 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart From a3bdbf3188a6dfd6cd77294a519213ea5bf881be Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 17 Sep 2019 15:41:49 +0200 Subject: [PATCH 040/296] Updated frontend to 20190917.1 (#26691) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 955c4b90cc6440..01823882f9d371 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190917.0" + "home-assistant-frontend==20190917.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b01efba8f0466e..43a22cb980c079 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190917.0 +home-assistant-frontend==20190917.1 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0e432bff8bb7be..c2be62c2325666 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.0 +home-assistant-frontend==20190917.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 297ed6bb866a46..b00194b0d91be8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.0 +home-assistant-frontend==20190917.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 15bb12f48eddf2416519f6568da1f1b118aeb533 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 17 Sep 2019 15:59:12 +0200 Subject: [PATCH 041/296] Fix release access for bram (#26693) --- azure-pipelines-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 7c88e615fa5baa..29e68a5d7acc16 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -43,7 +43,7 @@ stages: release="$(Build.SourceBranchName)" created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')" - if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480)$ ]]; then + if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten)$ ]]; then exit 0 fi From 12f68af1076f4dd1ae41b5b4006317f9a8679d5e Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 17 Sep 2019 08:53:12 -0700 Subject: [PATCH 042/296] Switch py_nextbus to py_nextbusnext (#26681) The orignal package maintainer seems to be unresponsive. I've forked the package and added the bug fixes to the new fork Fixes #24561 --- homeassistant/components/nextbus/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 63bdbf8a928e35..5c5a095c8f4cef 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/components/nextbus", "dependencies": [], "codeowners": ["@vividboarder"], - "requirements": ["py_nextbus==0.1.2"] + "requirements": ["py_nextbusnext==0.1.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index c2be62c2325666..80b79563e85bcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,7 @@ pyW215==0.6.0 pyW800rf32==0.1 # homeassistant.components.nextbus -py_nextbus==0.1.2 +py_nextbusnext==0.1.4 # homeassistant.components.noaa_tides # py_noaa==0.3.0 From ed13cab8d66b76582b51e1ef49c465134f918fe7 Mon Sep 17 00:00:00 2001 From: gibman Date: Tue, 17 Sep 2019 20:22:39 +0200 Subject: [PATCH 043/296] Disconnect velux on hass stop (#26266) * velux KLF200 device did not disconnect properly when rebooting the hass device. disconnect is now being called on the 'EVENT_HOMEASSISTANT_STOP' event * removed comment * removed comment * trigger bot * trigger bot * trigger bot * logging casing fixed. code moved from init. * logger level debug logger level moved from info to debug only config[DOMAIN] exposed to module imports moved to top * DOMAIN part of config passed to module. * removed trailing whitespaces etc. * black --fast changes * added missing docstring * D400 First line should end with a period * black formatting --- homeassistant/components/velux/__init__.py | 30 +++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 4c21bb7fdefbf5..51f615e68aaf5a 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -1,11 +1,12 @@ """Support for VELUX KLF 200 devices.""" import logging - import voluptuous as vol +from pyvlx import PyVLX +from pyvlx import PyVLXException from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP DOMAIN = "velux" DATA_VELUX = "data_velux" @@ -24,10 +25,9 @@ async def async_setup(hass, config): """Set up the velux component.""" - from pyvlx import PyVLXException - try: - hass.data[DATA_VELUX] = VeluxModule(hass, config) + hass.data[DATA_VELUX] = VeluxModule(hass, config[DOMAIN]) + hass.data[DATA_VELUX].setup() await hass.data[DATA_VELUX].async_start() except PyVLXException as ex: @@ -44,15 +44,27 @@ async def async_setup(hass, config): class VeluxModule: """Abstraction for velux component.""" - def __init__(self, hass, config): + def __init__(self, hass, domain_config): """Initialize for velux component.""" - from pyvlx import PyVLX + self.pyvlx = None + self._hass = hass + self._domain_config = domain_config + + def setup(self): + """Velux component setup.""" + + async def on_hass_stop(event): + """Close connection when hass stops.""" + _LOGGER.debug("Velux interface terminated") + await self.pyvlx.disconnect() - host = config[DOMAIN].get(CONF_HOST) - password = config[DOMAIN].get(CONF_PASSWORD) + self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + host = self._domain_config.get(CONF_HOST) + password = self._domain_config.get(CONF_PASSWORD) self.pyvlx = PyVLX(host=host, password=password) async def async_start(self): """Start velux component.""" + _LOGGER.debug("Velux interface started") await self.pyvlx.load_scenes() await self.pyvlx.load_nodes() From 4060f1346a2576c9e0799cbb7908c59182f6c88e Mon Sep 17 00:00:00 2001 From: Jesse Rizzo <32472573+jesserizzo@users.noreply.github.com> Date: Tue, 17 Sep 2019 13:24:03 -0500 Subject: [PATCH 044/296] Improve Envoy detection and support multiple Envoys (#26665) * Bump envoy_reader to 0.8.6, fix missing dependency * Support for optional name in config * Replace str.format with f-strings --- homeassistant/components/enphase_envoy/sensor.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 2cc46632ddaf58..13784e24d77fc9 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, + CONF_NAME, POWER_WATT, ENERGY_WATT_HOUR, ) @@ -44,6 +45,7 @@ vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( cv.ensure_list, [vol.In(list(SENSORS))] ), + vol.Optional(CONF_NAME, default=""): cv.string, } ) @@ -54,6 +56,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ip_address = config[CONF_IP_ADDRESS] monitored_conditions = config[CONF_MONITORED_CONDITIONS] + name = config[CONF_NAME] entities = [] # Iterate through the list of sensors @@ -66,14 +69,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= Envoy( ip_address, condition, - "{} {}".format(SENSORS[condition][0], inverter), + f"{name}{SENSORS[condition][0]} {inverter}", SENSORS[condition][1], ) ) else: entities.append( Envoy( - ip_address, condition, SENSORS[condition][0], SENSORS[condition][1] + ip_address, + condition, + f"{name}{SENSORS[condition][0]}", + SENSORS[condition][1], ) ) async_add_entities(entities) From 39edc45e4e82a10d1cbc6ffeccb8e83a3460a121 Mon Sep 17 00:00:00 2001 From: zewelor Date: Tue, 17 Sep 2019 20:29:46 +0200 Subject: [PATCH 045/296] Fix volumio set shuffle (#26660) --- homeassistant/components/volumio/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 8bd1952a6507d0..7c13488c3f573e 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -306,7 +306,7 @@ def async_mute_volume(self, mute): def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" return self.send_volumio_msg( - "commands", params={"cmd": "random", "value": str(shuffle)} + "commands", params={"cmd": "random", "value": str(shuffle).lower()} ) def async_select_source(self, source): From c17057de4b3b14792b9646d82e0a39bb27e5327d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 17 Sep 2019 21:00:17 +0200 Subject: [PATCH 046/296] Fix mysensors validation for composite entities (#26666) * Composite entities require multiple value types to be present in child values to function. Any of those value types should trigger an entity update if updated. * Always write platform v names as sets. * Run black. --- homeassistant/components/mysensors/const.py | 106 +++++++++--------- homeassistant/components/mysensors/helpers.py | 17 +-- 2 files changed, 63 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index d12ecd9d3a61a9..45f603a2cb44f8 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -26,72 +26,72 @@ UPDATE_DELAY = 0.1 BINARY_SENSOR_TYPES = { - "S_DOOR": "V_TRIPPED", - "S_MOTION": "V_TRIPPED", - "S_SMOKE": "V_TRIPPED", - "S_SPRINKLER": "V_TRIPPED", - "S_WATER_LEAK": "V_TRIPPED", - "S_SOUND": "V_TRIPPED", - "S_VIBRATION": "V_TRIPPED", - "S_MOISTURE": "V_TRIPPED", + "S_DOOR": {"V_TRIPPED"}, + "S_MOTION": {"V_TRIPPED"}, + "S_SMOKE": {"V_TRIPPED"}, + "S_SPRINKLER": {"V_TRIPPED"}, + "S_WATER_LEAK": {"V_TRIPPED"}, + "S_SOUND": {"V_TRIPPED"}, + "S_VIBRATION": {"V_TRIPPED"}, + "S_MOISTURE": {"V_TRIPPED"}, } -CLIMATE_TYPES = {"S_HVAC": "V_HVAC_FLOW_STATE"} +CLIMATE_TYPES = {"S_HVAC": {"V_HVAC_FLOW_STATE"}} -COVER_TYPES = {"S_COVER": ["V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"]} +COVER_TYPES = {"S_COVER": {"V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"}} -DEVICE_TRACKER_TYPES = {"S_GPS": "V_POSITION"} +DEVICE_TRACKER_TYPES = {"S_GPS": {"V_POSITION"}} LIGHT_TYPES = { - "S_DIMMER": ["V_DIMMER", "V_PERCENTAGE"], - "S_RGB_LIGHT": "V_RGB", - "S_RGBW_LIGHT": "V_RGBW", + "S_DIMMER": {"V_DIMMER", "V_PERCENTAGE"}, + "S_RGB_LIGHT": {"V_RGB"}, + "S_RGBW_LIGHT": {"V_RGBW"}, } -NOTIFY_TYPES = {"S_INFO": "V_TEXT"} +NOTIFY_TYPES = {"S_INFO": {"V_TEXT"}} SENSOR_TYPES = { - "S_SOUND": "V_LEVEL", - "S_VIBRATION": "V_LEVEL", - "S_MOISTURE": "V_LEVEL", - "S_INFO": "V_TEXT", - "S_GPS": "V_POSITION", - "S_TEMP": "V_TEMP", - "S_HUM": "V_HUM", - "S_BARO": ["V_PRESSURE", "V_FORECAST"], - "S_WIND": ["V_WIND", "V_GUST", "V_DIRECTION"], - "S_RAIN": ["V_RAIN", "V_RAINRATE"], - "S_UV": "V_UV", - "S_WEIGHT": ["V_WEIGHT", "V_IMPEDANCE"], - "S_POWER": ["V_WATT", "V_KWH", "V_VAR", "V_VA", "V_POWER_FACTOR"], - "S_DISTANCE": "V_DISTANCE", - "S_LIGHT_LEVEL": ["V_LIGHT_LEVEL", "V_LEVEL"], - "S_IR": "V_IR_RECEIVE", - "S_WATER": ["V_FLOW", "V_VOLUME"], - "S_CUSTOM": ["V_VAR1", "V_VAR2", "V_VAR3", "V_VAR4", "V_VAR5", "V_CUSTOM"], - "S_SCENE_CONTROLLER": ["V_SCENE_ON", "V_SCENE_OFF"], - "S_COLOR_SENSOR": "V_RGB", - "S_MULTIMETER": ["V_VOLTAGE", "V_CURRENT", "V_IMPEDANCE"], - "S_GAS": ["V_FLOW", "V_VOLUME"], - "S_WATER_QUALITY": ["V_TEMP", "V_PH", "V_ORP", "V_EC"], - "S_AIR_QUALITY": ["V_DUST_LEVEL", "V_LEVEL"], - "S_DUST": ["V_DUST_LEVEL", "V_LEVEL"], + "S_SOUND": {"V_LEVEL"}, + "S_VIBRATION": {"V_LEVEL"}, + "S_MOISTURE": {"V_LEVEL"}, + "S_INFO": {"V_TEXT"}, + "S_GPS": {"V_POSITION"}, + "S_TEMP": {"V_TEMP"}, + "S_HUM": {"V_HUM"}, + "S_BARO": {"V_PRESSURE", "V_FORECAST"}, + "S_WIND": {"V_WIND", "V_GUST", "V_DIRECTION"}, + "S_RAIN": {"V_RAIN", "V_RAINRATE"}, + "S_UV": {"V_UV"}, + "S_WEIGHT": {"V_WEIGHT", "V_IMPEDANCE"}, + "S_POWER": {"V_WATT", "V_KWH", "V_VAR", "V_VA", "V_POWER_FACTOR"}, + "S_DISTANCE": {"V_DISTANCE"}, + "S_LIGHT_LEVEL": {"V_LIGHT_LEVEL", "V_LEVEL"}, + "S_IR": {"V_IR_RECEIVE"}, + "S_WATER": {"V_FLOW", "V_VOLUME"}, + "S_CUSTOM": {"V_VAR1", "V_VAR2", "V_VAR3", "V_VAR4", "V_VAR5", "V_CUSTOM"}, + "S_SCENE_CONTROLLER": {"V_SCENE_ON", "V_SCENE_OFF"}, + "S_COLOR_SENSOR": {"V_RGB"}, + "S_MULTIMETER": {"V_VOLTAGE", "V_CURRENT", "V_IMPEDANCE"}, + "S_GAS": {"V_FLOW", "V_VOLUME"}, + "S_WATER_QUALITY": {"V_TEMP", "V_PH", "V_ORP", "V_EC"}, + "S_AIR_QUALITY": {"V_DUST_LEVEL", "V_LEVEL"}, + "S_DUST": {"V_DUST_LEVEL", "V_LEVEL"}, } SWITCH_TYPES = { - "S_LIGHT": "V_LIGHT", - "S_BINARY": "V_STATUS", - "S_DOOR": "V_ARMED", - "S_MOTION": "V_ARMED", - "S_SMOKE": "V_ARMED", - "S_SPRINKLER": "V_STATUS", - "S_WATER_LEAK": "V_ARMED", - "S_SOUND": "V_ARMED", - "S_VIBRATION": "V_ARMED", - "S_MOISTURE": "V_ARMED", - "S_IR": "V_IR_SEND", - "S_LOCK": "V_LOCK_STATUS", - "S_WATER_QUALITY": "V_STATUS", + "S_LIGHT": {"V_LIGHT"}, + "S_BINARY": {"V_STATUS"}, + "S_DOOR": {"V_ARMED"}, + "S_MOTION": {"V_ARMED"}, + "S_SMOKE": {"V_ARMED"}, + "S_SPRINKLER": {"V_STATUS"}, + "S_WATER_LEAK": {"V_ARMED"}, + "S_SOUND": {"V_ARMED"}, + "S_VIBRATION": {"V_ARMED"}, + "S_MOISTURE": {"V_ARMED"}, + "S_IR": {"V_IR_SEND"}, + "S_LOCK": {"V_LOCK_STATUS"}, + "S_WATER_QUALITY": {"V_STATUS"}, } diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index fda89158293bb2..f0e9b06b762a4c 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -121,20 +121,23 @@ def validate_child(gateway, node_id, child, value_type=None): child_type_name = next( (member.name for member in pres if member.value == child.type), None ) - value_types = [value_type] if value_type else [*child.values] - value_type_names = [ + value_types = {value_type} if value_type else {*child.values} + value_type_names = { member.name for member in set_req if member.value in value_types - ] + } platforms = TYPE_TO_PLATFORMS.get(child_type_name, []) if not platforms: _LOGGER.warning("Child type %s is not supported", child.type) return validated for platform in platforms: - v_names = FLAT_PLATFORM_TYPES[platform, child_type_name] - if not isinstance(v_names, list): - v_names = [v_names] - v_names = [v_name for v_name in v_names if v_name in value_type_names] + platform_v_names = FLAT_PLATFORM_TYPES[platform, child_type_name] + v_names = platform_v_names & value_type_names + if not v_names: + child_value_names = { + member.name for member in set_req if member.value in child.values + } + v_names = platform_v_names & child_value_names for v_name in v_names: child_schema_gen = SCHEMAS.get((platform, v_name), default_schema) From 10572a62b1d87c33386791c019d5fecebf5ceb0c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Sep 2019 21:12:54 +0200 Subject: [PATCH 047/296] Add support for automation description (#26662) * Add support for automation annotation * Rename annotation to description --- homeassistant/components/automation/__init__.py | 2 ++ homeassistant/components/config/automation.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 03eedd6d16213e..9e08a9cff1fc40 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -43,6 +43,7 @@ GROUP_NAME_ALL_AUTOMATIONS = "all automations" CONF_ALIAS = "alias" +CONF_DESCRIPTION = "description" CONF_HIDE_ENTITY = "hide_entity" CONF_CONDITION = "condition" @@ -95,6 +96,7 @@ def _platform_validator(config): # str on purpose CONF_ID: str, CONF_ALIAS: cv.string, + vol.Optional(CONF_DESCRIPTION): cv.string, vol.Optional(CONF_INITIAL_STATE): cv.boolean, vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 7ca71fc4f93dfb..17efdba3fb573f 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -53,7 +53,7 @@ def _write_value(self, hass, data, config_key, new_value): # Iterate through some keys that we want to have ordered in the output updated_value = OrderedDict() - for key in ("id", "alias", "trigger", "condition", "action"): + for key in ("id", "alias", "description", "trigger", "condition", "action"): if key in cur_value: updated_value[key] = cur_value[key] if key in new_value: From 504b8c7685089e13ed8ac0545622faab7b1b6a36 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Sep 2019 21:55:01 +0200 Subject: [PATCH 048/296] Fix translation, adjust trigger names (#26635) --- homeassistant/components/device_automation/const.py | 2 ++ .../components/device_automation/toggle_entity.py | 10 ++++++---- homeassistant/components/light/strings.json | 4 ++-- homeassistant/components/switch/strings.json | 8 ++++---- tests/components/device_automation/test_init.py | 4 ++-- tests/components/light/test_device_automation.py | 8 ++++---- tests/components/switch/test_device_automation.py | 8 ++++---- 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/device_automation/const.py b/homeassistant/components/device_automation/const.py index a668c78598a289..40bfc4ca0a13e4 100644 --- a/homeassistant/components/device_automation/const.py +++ b/homeassistant/components/device_automation/const.py @@ -4,3 +4,5 @@ CONF_TOGGLE = "toggle" CONF_TURN_OFF = "turn_off" CONF_TURN_ON = "turn_on" +CONF_TURNED_OFF = "turned_off" +CONF_TURNED_ON = "turned_on" diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 722b92e33f2603..1593e70771aea3 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -8,6 +8,8 @@ CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON, + CONF_TURNED_OFF, + CONF_TURNED_ON, ) from homeassistant.core import split_entity_id from homeassistant.const import ( @@ -53,12 +55,12 @@ { # Trigger when entity is turned off CONF_PLATFORM: "device", - CONF_TYPE: CONF_TURN_OFF, + CONF_TYPE: CONF_TURNED_OFF, }, { # Trigger when entity is turned on CONF_PLATFORM: "device", - CONF_TYPE: CONF_TURN_ON, + CONF_TYPE: CONF_TURNED_ON, }, ] @@ -87,7 +89,7 @@ vol.Required(CONF_DEVICE_ID): str, vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In([CONF_TURN_OFF, CONF_TURN_ON]), + vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), } ) @@ -136,7 +138,7 @@ def async_condition_from_config(config, config_validation): async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" trigger_type = config[CONF_TYPE] - if trigger_type == CONF_TURN_ON: + if trigger_type == CONF_TURNED_ON: from_state = "off" to_state = "on" else: diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 460114b143cc78..77b842ba07833a 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -10,8 +10,8 @@ "is_off": "{entity_name} is off" }, "trigger_type": { - "turn_on": "{entity_name} turned on", - "turn_off": "{entity_name} turned off" + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" } } } diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index 857b3763076143..77b842ba07833a 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -6,12 +6,12 @@ "turn_off": "Turn off {entity_name}" }, "condition_type": { - "turn_on": "{entity_name} turned on", - "turn_off": "{entity_name} turned off" + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" }, "trigger_type": { - "turn_on": "{entity_name} turned on", - "turn_off": "{entity_name} turned off" + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" } } } diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index a01dad03d46276..b05c04a16f1dc5 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -133,14 +133,14 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r { "platform": "device", "domain": "light", - "type": "turn_off", + "type": "turned_off", "device_id": device_entry.id, "entity_id": "light.test_5678", }, { "platform": "device", "domain": "light", - "type": "turn_on", + "type": "turned_on", "device_id": device_entry.id, "entity_id": "light.test_5678", }, diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py index 3525f1121c08e2..27b8b860d7227c 100644 --- a/tests/components/light/test_device_automation.py +++ b/tests/components/light/test_device_automation.py @@ -125,14 +125,14 @@ async def test_get_triggers(hass, device_reg, entity_reg): { "platform": "device", "domain": DOMAIN, - "type": "turn_off", + "type": "turned_off", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { "platform": "device", "domain": DOMAIN, - "type": "turn_on", + "type": "turned_on", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, @@ -163,7 +163,7 @@ async def test_if_fires_on_state_change(hass, calls): "domain": DOMAIN, "device_id": "", "entity_id": ent1.entity_id, - "type": "turn_on", + "type": "turned_on", }, "action": { "service": "test.automation", @@ -187,7 +187,7 @@ async def test_if_fires_on_state_change(hass, calls): "domain": DOMAIN, "device_id": "", "entity_id": ent1.entity_id, - "type": "turn_off", + "type": "turned_off", }, "action": { "service": "test.automation", diff --git a/tests/components/switch/test_device_automation.py b/tests/components/switch/test_device_automation.py index 3bd29a72a12549..1ebe4785761aa5 100644 --- a/tests/components/switch/test_device_automation.py +++ b/tests/components/switch/test_device_automation.py @@ -125,14 +125,14 @@ async def test_get_triggers(hass, device_reg, entity_reg): { "platform": "device", "domain": DOMAIN, - "type": "turn_off", + "type": "turned_off", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { "platform": "device", "domain": DOMAIN, - "type": "turn_on", + "type": "turned_on", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, @@ -163,7 +163,7 @@ async def test_if_fires_on_state_change(hass, calls): "domain": DOMAIN, "device_id": "", "entity_id": ent1.entity_id, - "type": "turn_on", + "type": "turned_on", }, "action": { "service": "test.automation", @@ -187,7 +187,7 @@ async def test_if_fires_on_state_change(hass, calls): "domain": DOMAIN, "device_id": "", "entity_id": ent1.entity_id, - "type": "turn_off", + "type": "turned_off", }, "action": { "service": "test.automation", From 9114ed36cd106f167d455f286b1d56c3be23b5de Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Tue, 17 Sep 2019 22:39:46 +0200 Subject: [PATCH 049/296] Fix cert expiry config flow check and update (#26638) * Fix typo in translations * Work on bug #26619 * readd the homeassistant.start event * Remove the callback * Added the executor_job for _test_connection * Update test_config_flow.py --- .../components/cert_expiry/.translations/en.json | 2 +- homeassistant/components/cert_expiry/__init__.py | 14 +++----------- .../components/cert_expiry/config_flow.py | 8 +++++--- homeassistant/components/cert_expiry/sensor.py | 16 +++++++++++++++- tests/components/cert_expiry/test_config_flow.py | 4 ++-- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json index 873dfee9a92bb9..51bd522f2c4491 100644 --- a/homeassistant/components/cert_expiry/.translations/en.json +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -21,4 +21,4 @@ }, "title": "Certificate Expiry" } -} \ No newline at end of file +} diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index ab68d5ba08bc43..7c7efea7333120 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -1,7 +1,5 @@ """The cert_expiry component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import callback from homeassistant.helpers.typing import HomeAssistantType @@ -13,13 +11,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Load the saved entities.""" - @callback - def async_start(_): - """Load the entry after the start event.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start) - + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) return True diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index dd3463fff95c4e..d73762ce882647 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -38,10 +38,12 @@ def _prt_in_configuration_exists(self, user_input) -> bool: return True return False - def _test_connection(self, user_input=None): + async def _test_connection(self, user_input=None): """Test connection to the server and try to get the certtificate.""" try: - get_cert(user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT)) + await self.hass.async_add_executor_job( + get_cert, user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT) + ) return True except socket.gaierror: self._errors[CONF_HOST] = "resolve_failed" @@ -59,7 +61,7 @@ async def async_step_user(self, user_input=None): if self._prt_in_configuration_exists(user_input): self._errors[CONF_HOST] = "host_port_exists" else: - if self._test_connection(user_input): + if await self._test_connection(user_input): host = user_input[CONF_HOST] name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) prt = user_input.get(CONF_PORT, DEFAULT_PORT) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index fccfb295c0fff2..b564cff7338584 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -9,7 +9,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT +from homeassistant.const import ( + CONF_NAME, + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_START, +) from homeassistant.helpers.entity import Entity from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT @@ -82,6 +87,15 @@ def available(self): """Icon to use in the frontend, if any.""" return self._available + async def async_added_to_hass(self): + """Once the entity is added we should update to get the initial data loaded.""" + + def do_update(_): + """Run the update method when the start event was fired.""" + self.update() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) + def update(self): """Fetch the certificate information.""" try: diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index f8c99496a563eb..f44e65512e35e5 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.cert_expiry.const import DEFAULT_PORT from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_coro NAME = "Cert Expiry test 1 2 3" PORT = 443 @@ -20,7 +20,7 @@ def mock_controller(): """Mock a successfull _prt_in_configuration_exists.""" with patch( "homeassistant.components.cert_expiry.config_flow.CertexpiryConfigFlow._test_connection", - return_value=True, + side_effect=lambda *_: mock_coro(True), ): yield From c6fc677f5b96c7fe63e4048a98cf45ac05293dd0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Sep 2019 13:45:48 -0700 Subject: [PATCH 050/296] Verify withings config (#26698) --- .../components/withings/config_flow.py | 5 ++- .../components/withings/strings.json | 5 ++- tests/components/withings/test_config_flow.py | 35 ++++++------------- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index 23cc74281e8b9b..f28a4f59d80195 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -88,7 +88,10 @@ async def async_step_import(self, user_input=None): async def async_step_user(self, user_input=None): """Create an entry for selecting a profile.""" - flow = self.hass.data.get(DATA_FLOW_IMPL, {}) + flow = self.hass.data.get(DATA_FLOW_IMPL) + + if not flow: + return self.async_abort(reason="no_flows") if user_input: return await self.async_step_auth(user_input) diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 88b8e6d5ea0944..1a99abc7255651 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -12,6 +12,9 @@ }, "create_entry": { "default": "Successfully authenticated with Withings for the selected profile." + }, + "abort": { + "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation." } } -} \ No newline at end of file +} diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 93b9a434b7f1ac..3ae9d11c3b6555 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -3,9 +3,7 @@ from asynctest import CoroutineMock, MagicMock import pytest -from homeassistant import setup, data_entry_flow -import homeassistant.components.api as api -import homeassistant.components.http as http +from homeassistant import data_entry_flow from homeassistant.components.withings import const from homeassistant.components.withings.config_flow import ( register_flow_implementation, @@ -24,27 +22,6 @@ def flow_handler_fixture(hass: HomeAssistantType): return flow_handler -@pytest.fixture(name="setup_hass") -async def setup_hass_fixture(hass: HomeAssistantType): - """Provide hass instance.""" - config = { - http.DOMAIN: {}, - api.DOMAIN: {"base_url": "http://localhost/"}, - const.DOMAIN: { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_secret", - const.PROFILES: ["Person 1", "Person 2"], - }, - } - - hass.data = {} - - await setup.async_setup_component(hass, "http", config) - await setup.async_setup_component(hass, "api", config) - - return hass - - def test_flow_handler_init(flow_handler: WithingsFlowHandler): """Test the init of the flow handler.""" assert not flow_handler.flow_profile @@ -173,3 +150,13 @@ async def test_auth_callback_view_get(hass: HomeAssistantType): "my_flow_id", {const.PROFILE: "my_profile", const.CODE: "my_code"} ) hass.config_entries.flow.async_configure.reset_mock() + + +async def test_init_without_config(hass): + """Try initializin a configg flow without it being configured.""" + result = await hass.config_entries.flow.async_init( + "withings", context={"source": "user"} + ) + + assert result["type"] == "abort" + assert result["reason"] == "no_flows" From d33ecbb5bed47e014637b31e5d5c3c87d33f3cd0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 17 Sep 2019 22:46:11 +0200 Subject: [PATCH 051/296] Updated frontend to 20190917.2 (#26696) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 01823882f9d371..3d5860d0a4350c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190917.1" + "home-assistant-frontend==20190917.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 43a22cb980c079..de5c823d999c28 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190917.1 +home-assistant-frontend==20190917.2 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 80b79563e85bcc..05a3e89b6757bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.1 +home-assistant-frontend==20190917.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b00194b0d91be8..07fc31ec6efd4e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.1 +home-assistant-frontend==20190917.2 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From a390cf7c6a0e36f13b7d551a7d9b4b752db96d78 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 18 Sep 2019 00:32:12 +0000 Subject: [PATCH 052/296] [ci skip] Translation update --- homeassistant/components/cert_expiry/.translations/en.json | 2 +- .../components/homekit_controller/.translations/fr.json | 2 +- homeassistant/components/light/.translations/ca.json | 4 +++- homeassistant/components/light/.translations/en.json | 4 +++- homeassistant/components/light/.translations/es.json | 4 +++- homeassistant/components/light/.translations/fr.json | 4 ++-- homeassistant/components/ps4/.translations/fr.json | 4 ++-- homeassistant/components/switch/.translations/ca.json | 6 +++++- homeassistant/components/switch/.translations/en.json | 6 +++++- homeassistant/components/switch/.translations/es.json | 6 +++++- homeassistant/components/upnp/.translations/ca.json | 4 ++++ homeassistant/components/withings/.translations/en.json | 3 +++ 12 files changed, 37 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json index 51bd522f2c4491..873dfee9a92bb9 100644 --- a/homeassistant/components/cert_expiry/.translations/en.json +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -21,4 +21,4 @@ }, "title": "Certificate Expiry" } -} +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/fr.json b/homeassistant/components/homekit_controller/.translations/fr.json index 15e50a4012701c..7f0566ddd42365 100644 --- a/homeassistant/components/homekit_controller/.translations/fr.json +++ b/homeassistant/components/homekit_controller/.translations/fr.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "Code d\u2019appairage" }, - "description": "Entrez votre code de jumelage HomeKit pour utiliser cet accessoire.", + "description": "Entrez votre code de jumelage HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire.", "title": "Appairer avec l'accessoire HomeKit" }, "user": { diff --git a/homeassistant/components/light/.translations/ca.json b/homeassistant/components/light/.translations/ca.json index 5017af8e576f64..4cdf3d9042cde2 100644 --- a/homeassistant/components/light/.translations/ca.json +++ b/homeassistant/components/light/.translations/ca.json @@ -11,7 +11,9 @@ }, "trigger_type": { "turn_off": "{name} apagat", - "turn_on": "{name} enc\u00e8s" + "turn_on": "{name} enc\u00e8s", + "turned_off": "{entity_name} apagat", + "turned_on": "{entity_name} enc\u00e8s" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/en.json b/homeassistant/components/light/.translations/en.json index 60ccbd99348d7d..225d64be231ea6 100644 --- a/homeassistant/components/light/.translations/en.json +++ b/homeassistant/components/light/.translations/en.json @@ -11,7 +11,9 @@ }, "trigger_type": { "turn_off": "{entity_name} turned off", - "turn_on": "{entity_name} turned on" + "turn_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/es.json b/homeassistant/components/light/.translations/es.json index fcbe835e4c2a9b..533c4b3c0f3486 100644 --- a/homeassistant/components/light/.translations/es.json +++ b/homeassistant/components/light/.translations/es.json @@ -11,7 +11,9 @@ }, "trigger_type": { "turn_off": "{entity_name} apagada", - "turn_on": "{entity_name} encendida" + "turn_on": "{entity_name} encendida", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json index 6ab87274409320..f0aabef2ae742d 100644 --- a/homeassistant/components/light/.translations/fr.json +++ b/homeassistant/components/light/.translations/fr.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} est allum\u00e9" }, "trigger_type": { - "turn_off": "{name} d\u00e9sactiv\u00e9", - "turn_on": "{name} activ\u00e9" + "turn_off": "{entity_name} d\u00e9sactiv\u00e9", + "turn_on": "{entity_name} activ\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/fr.json b/homeassistant/components/ps4/.translations/fr.json index 03baf0c032e3a3..991222d45be78b 100644 --- a/homeassistant/components/ps4/.translations/fr.json +++ b/homeassistant/components/ps4/.translations/fr.json @@ -4,8 +4,8 @@ "credential_error": "Erreur lors de l'extraction des informations d'identification.", "devices_configured": "Tous les p\u00e9riph\u00e9riques trouv\u00e9s sont d\u00e9j\u00e0 configur\u00e9s.", "no_devices_found": "Aucun appareil PlayStation 4 trouv\u00e9 sur le r\u00e9seau.", - "port_987_bind_error": "Impossible de se connecter au port 997.", - "port_997_bind_error": "Impossible de se connecter au port 997." + "port_987_bind_error": "Impossible de se connecter au port 997. Reportez-vous \u00e0 la [documentation] (https://www.home-assistant.io/components/ps4/) pour plus d'informations.", + "port_997_bind_error": "Impossible de se connecter au port 997. Reportez-vous \u00e0 la [documentation] (https://www.home-assistant.io/components/ps4/) pour plus d'informations." }, "error": { "credential_timeout": "Le service d'informations d'identification a expir\u00e9. Appuyez sur soumettre pour red\u00e9marrer.", diff --git a/homeassistant/components/switch/.translations/ca.json b/homeassistant/components/switch/.translations/ca.json index c97565ddfe6a7e..6fea704f756d49 100644 --- a/homeassistant/components/switch/.translations/ca.json +++ b/homeassistant/components/switch/.translations/ca.json @@ -6,12 +6,16 @@ "turn_on": "Activa {entity_name}" }, "condition_type": { + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s", "turn_off": "{entity_name} desactivat", "turn_on": "{entity_name} activat" }, "trigger_type": { "turn_off": "{entity_name} desactivat", - "turn_on": "{entity_name} activat" + "turn_on": "{entity_name} activat", + "turned_off": "{entity_name} apagat", + "turned_on": "{entity_name} enc\u00e8s" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/en.json b/homeassistant/components/switch/.translations/en.json index 5be333cbf13bf7..ed036023755c40 100644 --- a/homeassistant/components/switch/.translations/en.json +++ b/homeassistant/components/switch/.translations/en.json @@ -6,12 +6,16 @@ "turn_on": "Turn on {entity_name}" }, "condition_type": { + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on", "turn_off": "{entity_name} turned off", "turn_on": "{entity_name} turned on" }, "trigger_type": { "turn_off": "{entity_name} turned off", - "turn_on": "{entity_name} turned on" + "turn_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/es.json b/homeassistant/components/switch/.translations/es.json index 987d13a39403e8..b38928fa753dce 100644 --- a/homeassistant/components/switch/.translations/es.json +++ b/homeassistant/components/switch/.translations/es.json @@ -6,12 +6,16 @@ "turn_on": "Encender {entity_name}" }, "condition_type": { + "is_off": "{entity_name} est\u00e1 apagada", + "is_on": "{entity_name} est\u00e1 encendida", "turn_off": "{entity_name} apagado", "turn_on": "{entity_name} encendido" }, "trigger_type": { "turn_off": "{entity_name} apagado", - "turn_on": "{entity_name} encendido" + "turn_on": "{entity_name} encendido", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/ca.json b/homeassistant/components/upnp/.translations/ca.json index 161b5d85599147..28ad9ce954d995 100644 --- a/homeassistant/components/upnp/.translations/ca.json +++ b/homeassistant/components/upnp/.translations/ca.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Activa, com a m\u00ednim, els sensors o l'assignaci\u00f3 de ports", "single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de UPnP/IGD." }, + "error": { + "one": "un", + "other": "altre" + }, "step": { "confirm": { "description": "Vols configurar UPnP/IGD?", diff --git a/homeassistant/components/withings/.translations/en.json b/homeassistant/components/withings/.translations/en.json index 2b906dd80030fb..16ce491e776d02 100644 --- a/homeassistant/components/withings/.translations/en.json +++ b/homeassistant/components/withings/.translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation." + }, "create_entry": { "default": "Successfully authenticated with Withings for the selected profile." }, From 72baf563fa040150a24b560c3052831475faf3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 18 Sep 2019 08:30:59 +0300 Subject: [PATCH 053/296] Add alternative name for Tibber sensors (#26685) * Add alternative name for Tibber sensors * refactor tibber sensor --- homeassistant/components/tibber/sensor.py | 68 +++++++++-------------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 3dfe0265bdeef5..a5a7f320d93315 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -44,8 +44,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(dev, True) -class TibberSensorElPrice(Entity): - """Representation of an Tibber sensor for el price.""" +class TibberSensor(Entity): + """Representation of a generic Tibber sensor.""" def __init__(self, tibber_home): """Initialize the sensor.""" @@ -54,10 +54,25 @@ def __init__(self, tibber_home): self._state = None self._is_available = False self._device_state_attributes = {} - self._unit_of_measurement = self._tibber_home.price_unit - self._name = "Electricity price {}".format( - tibber_home.info["viewer"]["home"]["appNickname"] - ) + self._name = tibber_home.info["viewer"]["home"]["appNickname"] + if self._name is None: + self._name = tibber_home.info["viewer"]["home"]["address"].get( + "address1", "" + ) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._device_state_attributes + + @property + def state(self): + """Return the state of the device.""" + return self._state + + +class TibberSensorElPrice(TibberSensor): + """Representation of a Tibber sensor for el price.""" async def async_update(self): """Get the latest data and updates the states.""" @@ -86,11 +101,6 @@ async def async_update(self): self._device_state_attributes.update(attrs) self._is_available = self._state is not None - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._device_state_attributes - @property def available(self): """Return True if entity is available.""" @@ -99,12 +109,7 @@ def available(self): @property def name(self): """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state + return "Electricity price {}".format(self._name) @property def icon(self): @@ -114,7 +119,7 @@ def icon(self): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return self._unit_of_measurement + return self._tibber_home.price_unit @property def unique_id(self): @@ -139,17 +144,8 @@ async def _fetch_data(self): ]["estimatedAnnualConsumption"] -class TibberSensorRT(Entity): - """Representation of an Tibber sensor for real time consumption.""" - - def __init__(self, tibber_home): - """Initialize the sensor.""" - self._tibber_home = tibber_home - self._state = None - self._device_state_attributes = {} - self._unit_of_measurement = "W" - nickname = tibber_home.info["viewer"]["home"]["appNickname"] - self._name = f"Real time consumption {nickname}" +class TibberSensorRT(TibberSensor): + """Representation of a Tibber sensor for real time consumption.""" async def async_added_to_hass(self): """Start unavailability tracking.""" @@ -175,11 +171,6 @@ async def _async_callback(self, payload): self.async_schedule_update_ha_state() - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._device_state_attributes - @property def available(self): """Return True if entity is available.""" @@ -188,18 +179,13 @@ def available(self): @property def name(self): """Return the name of the sensor.""" - return self._name + return "Real time consumption {}".format(self._name) @property def should_poll(self): """Return the polling state.""" return False - @property - def state(self): - """Return the state of the device.""" - return self._state - @property def icon(self): """Return the icon to use in the frontend.""" @@ -208,7 +194,7 @@ def icon(self): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return self._unit_of_measurement + return "W" @property def unique_id(self): From 8a39924b37486bf806467eafefaa654c33ada45b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 18 Sep 2019 12:47:26 +0200 Subject: [PATCH 054/296] deCONZ improve light tests (#26697) * Improve light tests * Small improvements on light and group classes --- homeassistant/components/deconz/cover.py | 1 - homeassistant/components/deconz/light.py | 16 +- homeassistant/components/deconz/switch.py | 1 - tests/components/deconz/test_cover.py | 190 +++++------ tests/components/deconz/test_light.py | 365 ++++++++++++---------- tests/components/deconz/test_switch.py | 218 +++++++------ 6 files changed, 431 insertions(+), 360 deletions(-) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index b82144d37c739a..bcd408c25a7591 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -17,7 +17,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index ec1dfd2bcb1901..bf4b05089a84c0 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -34,7 +34,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): @@ -194,9 +193,6 @@ def device_state_attributes(self): attributes = {} attributes["is_deconz_group"] = self._device.type == "LightGroup" - if self._device.type == "LightGroup": - attributes["all_on"] = self._device.all_on - return attributes @@ -207,9 +203,7 @@ def __init__(self, device, gateway): """Set up group and create an unique id.""" super().__init__(device, gateway) - self._unique_id = "{}-{}".format( - self.gateway.api.config.bridgeid, self._device.deconz_id - ) + self._unique_id = f"{self.gateway.api.config.bridgeid}-{self._device.deconz_id}" @property def unique_id(self): @@ -228,3 +222,11 @@ def device_info(self): "name": self._device.name, "via_device": (DECONZ_DOMAIN, bridgeid), } + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = dict(super().device_state_attributes) + attributes["all_on"] = self._device.all_on + + return attributes diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index b1fd4b10f46065..1b51256580aeb7 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -10,7 +10,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 2de70f6d247c30..246c2bae7c561c 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -1,82 +1,83 @@ """deCONZ cover platform tests.""" -from unittest.mock import Mock, patch +from copy import deepcopy + +from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.components.deconz.const import COVER_TYPES -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.components.cover as cover -from tests.common import mock_coro - -SUPPORTED_COVERS = { +COVERS = { "1": { - "id": "Cover 1 id", - "name": "Cover 1 name", + "id": "Level controllable cover id", + "name": "Level controllable cover", "type": "Level controllable output", "state": {"bri": 255, "on": False, "reachable": True}, "modelid": "Not zigbee spec", "uniqueid": "00:00:00:00:00:00:00:00-00", }, "2": { - "id": "Cover 2 id", - "name": "Cover 2 name", + "id": "Window covering device id", + "name": "Window covering device", "type": "Window covering device", "state": {"bri": 255, "on": True, "reachable": True}, "modelid": "lumi.curtain", + "uniqueid": "00:00:00:00:00:00:00:01-00", }, -} - -UNSUPPORTED_COVER = { - "1": { - "id": "Cover id", - "name": "Unsupported switch", + "3": { + "id": "Unsupported cover id", + "name": "Unsupported cover", "type": "Not a cover", - "state": {}, - } + "state": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, } +BRIDGEID = "0123456789" ENTRY_CONFIG = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: "0123456789", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, +} -async def setup_gateway(hass, data): - """Load the deCONZ cover platform.""" - from pydeconz import DeconzSession +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - loop = Mock() - session = Mock() +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, system_options={}, + options=options, + entry_id="1", ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - - await hass.config_entries.async_forward_entry_setup(config_entry, "cover") - # To flush out the service call to update the group + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() - return gateway + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] async def test_platform_manually_configured(hass): @@ -92,64 +93,69 @@ async def test_platform_manually_configured(hass): async def test_no_covers(hass): """Test that no cover entities are created.""" - gateway = await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_cover(hass): """Test that all supported cover entities are created.""" - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - gateway = await setup_gateway(hass, {"lights": SUPPORTED_COVERS}) - assert "cover.cover_1_name" in gateway.deconz_ids - assert len(SUPPORTED_COVERS) == len(COVER_TYPES) - assert len(hass.states.async_all()) == 3 - - cover_1 = hass.states.get("cover.cover_1_name") - assert cover_1 is not None - assert cover_1.state == "open" - - gateway.api.lights["1"].async_update({}) - - await hass.services.async_call( - "cover", "open_cover", {"entity_id": "cover.cover_1_name"}, blocking=True - ) - await hass.services.async_call( - "cover", "close_cover", {"entity_id": "cover.cover_1_name"}, blocking=True - ) - await hass.services.async_call( - "cover", "stop_cover", {"entity_id": "cover.cover_1_name"}, blocking=True + data = deepcopy(DECONZ_WEB_REQUEST) + data["lights"] = deepcopy(COVERS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) + assert "cover.level_controllable_cover" in gateway.deconz_ids + assert "cover.window_covering_device" in gateway.deconz_ids + assert "cover.unsupported_cover" not in gateway.deconz_ids + assert len(hass.states.async_all()) == 5 - await hass.services.async_call( - "cover", "close_cover", {"entity_id": "cover.cover_2_name"}, blocking=True - ) + level_controllable_cover = hass.states.get("cover.level_controllable_cover") + assert level_controllable_cover.state == "open" + level_controllable_cover_device = gateway.api.lights["1"] -async def test_add_new_cover(hass): - """Test successful creation of cover entity.""" - data = {} - gateway = await setup_gateway(hass, data) - cover = Mock() - cover.name = "name" - cover.type = "Level controllable output" - cover.uniqueid = "1" - cover.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("light"), [cover]) + level_controllable_cover_device.async_update({"state": {"on": True}}) await hass.async_block_till_done() - assert "cover.name" in gateway.deconz_ids - - -async def test_unsupported_cover(hass): - """Test that unsupported covers are not created.""" - await setup_gateway(hass, {"lights": UNSUPPORTED_COVER}) - assert len(hass.states.async_all()) == 0 - -async def test_unload_cover(hass): - """Test that it works to unload switch entities.""" - gateway = await setup_gateway(hass, {"lights": SUPPORTED_COVERS}) - - await gateway.async_reset() - - assert len(hass.states.async_all()) == 1 + level_controllable_cover = hass.states.get("cover.level_controllable_cover") + assert level_controllable_cover.state == "closed" + + with patch.object( + level_controllable_cover_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + cover.DOMAIN, + cover.SERVICE_OPEN_COVER, + {"entity_id": "cover.level_controllable_cover"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/1/state", {"on": False}) + + with patch.object( + level_controllable_cover_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + cover.DOMAIN, + cover.SERVICE_CLOSE_COVER, + {"entity_id": "cover.level_controllable_cover"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/1/state", {"on": True, "bri": 255}) + + with patch.object( + level_controllable_cover_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + cover.DOMAIN, + cover.SERVICE_STOP_COVER, + {"entity_id": "cover.level_controllable_cover"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/1/state", {"bri_inc": 0}) diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index ecce762f51c4f3..50a5b2adacabcb 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -1,49 +1,28 @@ """deCONZ light platform tests.""" -from unittest.mock import Mock, patch +from copy import deepcopy + +from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.components.light as light -from tests.common import mock_coro - - -LIGHT = { +GROUPS = { "1": { - "id": "Light 1 id", - "name": "Light 1 name", - "state": { - "on": True, - "bri": 255, - "colormode": "xy", - "xy": (500, 500), - "reachable": True, - }, - "uniqueid": "00:00:00:00:00:00:00:00-00", - }, - "2": { - "id": "Light 2 id", - "name": "Light 2 name", - "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True}, - }, -} - -GROUP = { - "1": { - "id": "Group 1 id", - "name": "Group 1 name", + "id": "Light group id", + "name": "Light group", "type": "LightGroup", - "state": {}, + "state": {"all_on": False, "any_on": True}, "action": {}, "scenes": [], "lights": ["1", "2"], }, "2": { - "id": "Group 2 id", - "name": "Group 2 name", + "id": "Empty group id", + "name": "Empty group", + "type": "LightGroup", "state": {}, "action": {}, "scenes": [], @@ -51,60 +30,80 @@ }, } -SWITCH = { +LIGHTS = { "1": { - "id": "Switch 1 id", - "name": "Switch 1 name", + "id": "RGB light id", + "name": "RGB light", + "state": { + "on": True, + "bri": 255, + "colormode": "xy", + "effect": "colorloop", + "xy": (500, 500), + "reachable": True, + }, + "type": "Extended color light", + "uniqueid": "00:00:00:00:00:00:00:00-00", + }, + "2": { + "id": "Tunable white light id", + "name": "Tunable white light", + "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True}, + "type": "Tunable white light", + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "3": { + "id": "On off switch id", + "name": "On off switch", "type": "On/Off plug-in unit", - "state": {}, - } + "state": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, } +BRIDGEID = "0123456789" ENTRY_CONFIG = { deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: "0123456789", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } -ENTRY_OPTIONS = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, } +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} -async def setup_gateway(hass, data, allow_deconz_groups=True): - """Load the deCONZ light platform.""" - from pydeconz import DeconzSession - - loop = Mock() - session = Mock() - - ENTRY_OPTIONS[deconz.const.CONF_ALLOW_DECONZ_GROUPS] = allow_deconz_groups +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, system_options={}, - options=ENTRY_OPTIONS, + options=options, + entry_id="1", ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - await hass.config_entries.async_forward_entry_setup(config_entry, "light") - # To flush out the service call to update the group + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() - return gateway + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] async def test_platform_manually_configured(hass): @@ -120,119 +119,157 @@ async def test_platform_manually_configured(hass): async def test_no_lights_or_groups(hass): """Test that no lights or groups entities are created.""" - gateway = await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_lights_and_groups(hass): """Test that lights or groups entities are created.""" - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - gateway = await setup_gateway(hass, {"lights": LIGHT, "groups": GROUP}) - assert "light.light_1_name" in gateway.deconz_ids - assert "light.light_2_name" in gateway.deconz_ids - assert "light.group_1_name" in gateway.deconz_ids - assert "light.group_2_name" not in gateway.deconz_ids - assert len(hass.states.async_all()) == 4 - - lamp_1 = hass.states.get("light.light_1_name") - assert lamp_1 is not None - assert lamp_1.state == "on" - assert lamp_1.attributes["brightness"] == 255 - assert lamp_1.attributes["hs_color"] == (224.235, 100.0) - - light_2 = hass.states.get("light.light_2_name") - assert light_2 is not None - assert light_2.state == "on" - assert light_2.attributes["color_temp"] == 2500 - - gateway.api.lights["1"].async_update({}) - - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.light_1_name", - "color_temp": 2500, - "brightness": 200, - "transition": 5, - "flash": "short", - "effect": "colorloop", - }, - blocking=True, - ) - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.light_1_name", - "hs_color": (20, 30), - "flash": "long", - "effect": "None", - }, - blocking=True, - ) - await hass.services.async_call( - "light", - "turn_off", - {"entity_id": "light.light_1_name", "transition": 5, "flash": "short"}, - blocking=True, - ) - await hass.services.async_call( - "light", - "turn_off", - {"entity_id": "light.light_1_name", "flash": "long"}, - blocking=True, + data = deepcopy(DECONZ_WEB_REQUEST) + data["groups"] = deepcopy(GROUPS) + data["lights"] = deepcopy(LIGHTS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) - - -async def test_add_new_light(hass): - """Test successful creation of light entities.""" - gateway = await setup_gateway(hass, {}) - light = Mock() - light.name = "name" - light.uniqueid = "1" - light.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("light"), [light]) + assert "light.rgb_light" in gateway.deconz_ids + assert "light.tunable_white_light" in gateway.deconz_ids + assert "light.light_group" in gateway.deconz_ids + assert "light.empty_group" not in gateway.deconz_ids + assert "light.on_off_switch" not in gateway.deconz_ids + # 4 entities + 2 groups (one for switches and one for lights) + assert len(hass.states.async_all()) == 6 + + rgb_light = hass.states.get("light.rgb_light") + assert rgb_light.state == "on" + assert rgb_light.attributes["brightness"] == 255 + assert rgb_light.attributes["hs_color"] == (224.235, 100.0) + assert rgb_light.attributes["is_deconz_group"] is False + + tunable_white_light = hass.states.get("light.tunable_white_light") + assert tunable_white_light.state == "on" + assert tunable_white_light.attributes["color_temp"] == 2500 + + light_group = hass.states.get("light.light_group") + assert light_group.state == "on" + assert light_group.attributes["all_on"] is False + + empty_group = hass.states.get("light.empty_group") + assert empty_group is None + + rgb_light_device = gateway.api.lights["1"] + + rgb_light_device.async_update({"state": {"on": False}}) await hass.async_block_till_done() - assert "light.name" in gateway.deconz_ids + rgb_light = hass.states.get("light.rgb_light") + assert rgb_light.state == "off" + + with patch.object( + rgb_light_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_ON, + { + "entity_id": "light.rgb_light", + "color_temp": 2500, + "brightness": 200, + "transition": 5, + "flash": "short", + "effect": "colorloop", + }, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "/lights/1/state", + { + "ct": 2500, + "bri": 200, + "transitiontime": 50, + "alert": "select", + "effect": "colorloop", + }, + ) -async def test_add_new_group(hass): - """Test successful creation of group entities.""" - gateway = await setup_gateway(hass, {}) - group = Mock() - group.name = "name" - group.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("group"), [group]) - await hass.async_block_till_done() - assert "light.name" in gateway.deconz_ids + with patch.object( + rgb_light_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_ON, + { + "entity_id": "light.rgb_light", + "hs_color": (20, 30), + "flash": "long", + "effect": "None", + }, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "/lights/1/state", + {"xy": (0.411, 0.351), "alert": "lselect", "effect": "none"}, + ) + with patch.object( + rgb_light_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_OFF, + {"entity_id": "light.rgb_light", "transition": 5, "flash": "short"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "/lights/1/state", {"bri": 0, "transitiontime": 50, "alert": "select"} + ) -async def test_do_not_add_deconz_groups(hass): - """Test that clip sensors can be ignored.""" - gateway = await setup_gateway(hass, {}, allow_deconz_groups=False) - group = Mock() - group.name = "name" - group.type = "LightGroup" - group.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("group"), [group]) - await hass.async_block_till_done() - assert len(gateway.deconz_ids) == 0 + with patch.object( + rgb_light_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_OFF, + {"entity_id": "light.rgb_light", "flash": "long"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/1/state", {"alert": "lselect"}) -async def test_no_switch(hass): - """Test that a switch doesn't get created as a light entity.""" - gateway = await setup_gateway(hass, {"lights": SWITCH}) - assert len(gateway.deconz_ids) == 0 - assert len(hass.states.async_all()) == 0 +async def test_disable_light_groups(hass): + """Test successful creation of sensor entities.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["groups"] = deepcopy(GROUPS) + data["lights"] = deepcopy(LIGHTS) + gateway = await setup_deconz_integration( + hass, + ENTRY_CONFIG, + options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: False}, + get_state_response=data, + ) + assert "light.rgb_light" in gateway.deconz_ids + assert "light.tunable_white_light" in gateway.deconz_ids + assert "light.light_group" not in gateway.deconz_ids + assert "light.empty_group" not in gateway.deconz_ids + assert "light.on_off_switch" not in gateway.deconz_ids + # 4 entities + 2 groups (one for switches and one for lights) + assert len(hass.states.async_all()) == 5 + rgb_light = hass.states.get("light.rgb_light") + assert rgb_light is not None -async def test_unload_light(hass): - """Test that it works to unload switch entities.""" - gateway = await setup_gateway(hass, {"lights": LIGHT, "groups": GROUP}) + tunable_white_light = hass.states.get("light.tunable_white_light") + assert tunable_white_light is not None - await gateway.async_reset() + light_group = hass.states.get("light.light_group") + assert light_group is None - # Group.all_lights will not be removed - assert len(hass.states.async_all()) == 1 + empty_group = hass.states.get("light.empty_group") + assert empty_group is None diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 6b691bcab8e038..c574ed8911e063 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -1,86 +1,89 @@ """deCONZ switch platform tests.""" -from unittest.mock import Mock, patch +from copy import deepcopy + +from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.components.deconz.const import SWITCH_TYPES -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component -import homeassistant.components.switch as switch -from tests.common import mock_coro +import homeassistant.components.switch as switch -SUPPORTED_SWITCHES = { +SWITCHES = { "1": { - "id": "Switch 1 id", - "name": "Switch 1 name", + "id": "On off switch id", + "name": "On off switch", "type": "On/Off plug-in unit", "state": {"on": True, "reachable": True}, "uniqueid": "00:00:00:00:00:00:00:00-00", }, "2": { - "id": "Switch 2 id", - "name": "Switch 2 name", + "id": "Smart plug id", + "name": "Smart plug", "type": "Smart plug", - "state": {"on": True, "reachable": True}, + "state": {"on": False, "reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:01-00", }, "3": { - "id": "Switch 3 id", - "name": "Switch 3 name", + "id": "Warning device id", + "name": "Warning device", "type": "Warning device", "state": {"alert": "lselect", "reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", }, -} - -UNSUPPORTED_SWITCH = { - "1": { - "id": "Switch id", + "4": { + "id": "Unsupported switch id", "name": "Unsupported switch", "type": "Not a smart plug", - "state": {}, - } + "state": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:03-00", + }, } +BRIDGEID = "0123456789" ENTRY_CONFIG = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: "0123456789", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, +} -async def setup_gateway(hass, data): - """Load the deCONZ switch platform.""" - from pydeconz import DeconzSession +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - loop = Mock() - session = Mock() +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, system_options={}, + options=options, + entry_id="1", ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - - await hass.config_entries.async_forward_entry_setup(config_entry, "switch") - # To flush out the service call to update the group + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() - return gateway + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] async def test_platform_manually_configured(hass): @@ -96,68 +99,93 @@ async def test_platform_manually_configured(hass): async def test_no_switches(hass): """Test that no switch entities are created.""" - gateway = await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_switches(hass): """Test that all supported switch entities are created.""" - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - gateway = await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES}) - assert "switch.switch_1_name" in gateway.deconz_ids - assert "switch.switch_2_name" in gateway.deconz_ids - assert "switch.switch_3_name" in gateway.deconz_ids - assert len(SUPPORTED_SWITCHES) == len(SWITCH_TYPES) - assert len(hass.states.async_all()) == 4 - - switch_1 = hass.states.get("switch.switch_1_name") - assert switch_1 is not None - assert switch_1.state == "on" - switch_3 = hass.states.get("switch.switch_3_name") - assert switch_3 is not None - assert switch_3.state == "on" - - gateway.api.lights["1"].async_update({}) - - await hass.services.async_call( - "switch", "turn_on", {"entity_id": "switch.switch_1_name"}, blocking=True - ) - await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.switch_1_name"}, blocking=True - ) - - await hass.services.async_call( - "switch", "turn_on", {"entity_id": "switch.switch_3_name"}, blocking=True - ) - await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.switch_3_name"}, blocking=True + data = deepcopy(DECONZ_WEB_REQUEST) + data["lights"] = deepcopy(SWITCHES) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) + assert "switch.on_off_switch" in gateway.deconz_ids + assert "switch.smart_plug" in gateway.deconz_ids + assert "switch.warning_device" in gateway.deconz_ids + assert "switch.unsupported_switch" not in gateway.deconz_ids + assert len(hass.states.async_all()) == 6 + on_off_switch = hass.states.get("switch.on_off_switch") + assert on_off_switch.state == "on" -async def test_add_new_switch(hass): - """Test successful creation of switch entity.""" - gateway = await setup_gateway(hass, {}) - switch = Mock() - switch.name = "name" - switch.type = "Smart plug" - switch.uniqueid = "1" - switch.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("light"), [switch]) - await hass.async_block_till_done() - assert "switch.name" in gateway.deconz_ids + smart_plug = hass.states.get("switch.smart_plug") + assert smart_plug.state == "off" + warning_device = hass.states.get("switch.warning_device") + assert warning_device.state == "on" -async def test_unsupported_switch(hass): - """Test that unsupported switches are not created.""" - await setup_gateway(hass, {"lights": UNSUPPORTED_SWITCH}) - assert len(hass.states.async_all()) == 0 + on_off_switch_device = gateway.api.lights["1"] + warning_device_device = gateway.api.lights["3"] + on_off_switch_device.async_update({"state": {"on": False}}) + warning_device_device.async_update({"state": {"alert": None}}) + await hass.async_block_till_done() -async def test_unload_switch(hass): - """Test that it works to unload switch entities.""" - gateway = await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES}) + on_off_switch = hass.states.get("switch.on_off_switch") + assert on_off_switch.state == "off" - await gateway.async_reset() + warning_device = hass.states.get("switch.warning_device") + assert warning_device.state == "off" - assert len(hass.states.async_all()) == 1 + with patch.object( + on_off_switch_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + switch.DOMAIN, + switch.SERVICE_TURN_ON, + {"entity_id": "switch.on_off_switch"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/1/state", {"on": True}) + + with patch.object( + on_off_switch_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + switch.DOMAIN, + switch.SERVICE_TURN_OFF, + {"entity_id": "switch.on_off_switch"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/1/state", {"on": False}) + + with patch.object( + warning_device_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + switch.DOMAIN, + switch.SERVICE_TURN_ON, + {"entity_id": "switch.warning_device"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/3/state", {"alert": "lselect"}) + + with patch.object( + warning_device_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + switch.DOMAIN, + switch.SERVICE_TURN_OFF, + {"entity_id": "switch.warning_device"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/3/state", {"alert": "none"}) From fe5a4cef7f7d93fb55b6a4cc339e54ba93ea00ec Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 18 Sep 2019 15:37:04 +0200 Subject: [PATCH 055/296] Updated frontend to 20190918.0 (#26704) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 3d5860d0a4350c..628099a47d2c15 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190917.2" + "home-assistant-frontend==20190918.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index de5c823d999c28..bbeb228f3459e0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190917.2 +home-assistant-frontend==20190918.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 05a3e89b6757bf..415c8e514f58b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.2 +home-assistant-frontend==20190918.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07fc31ec6efd4e..5ff52cf2255c4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.2 +home-assistant-frontend==20190918.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 9cd5c5471df061f1a032944dd4f38b193a86d047 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Wed, 18 Sep 2019 20:00:12 +0400 Subject: [PATCH 056/296] Hide "PTZ is not available on this camera" warning (#26649) * Hide "PTZ is not available" warning * Change log level to "debug" --- homeassistant/components/onvif/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 0635a2d1f11bb1..4fdd513f840f28 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -282,7 +282,7 @@ def setup_ptz(self): """Set up PTZ if available.""" _LOGGER.debug("Setting up the ONVIF PTZ service") if self._camera.get_service("ptz", create=False) is None: - _LOGGER.warning("PTZ is not available on this camera") + _LOGGER.debug("PTZ is not available") else: self._ptz_service = self._camera.create_ptz_service() _LOGGER.debug("Completed set up of the ONVIF camera component") From ce42b46ccd68897080a539623889b8cac12de324 Mon Sep 17 00:00:00 2001 From: zewelor Date: Wed, 18 Sep 2019 19:07:07 +0200 Subject: [PATCH 057/296] Fix yeelight inheritance order (#26706) --- homeassistant/components/yeelight/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 171f25128c6640..b47cdb981612e4 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -822,7 +822,7 @@ def _predefined_effects(self): class YeelightWhiteTempWithoutNightlightSwitch( - YeelightGenericLight, YeelightWhiteTempLightsupport + YeelightWhiteTempLightsupport, YeelightGenericLight ): """White temp light, when nightlight switch is not set to light.""" @@ -831,7 +831,7 @@ def _brightness_property(self): return "current_brightness" -class YeelightWithNightLight(YeelightGenericLight, YeelightWhiteTempLightsupport): +class YeelightWithNightLight(YeelightWhiteTempLightsupport, YeelightGenericLight): """Representation of a Yeelight with nightlight support. It represents case when nightlight switch is set to light. From 886d8bd6e27122a074739bcb8b155fa22ce53633 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 18 Sep 2019 19:07:32 +0200 Subject: [PATCH 058/296] deCONZ rewrite sensor tests (#26679) * Improve binary sensor tests * Fix sensor tests * Improve readability of binary sensor * Fix climate tests Fix sensor platform not loading climate devices as sensors * Add test to verify adding new sensor after start up --- homeassistant/components/deconz/sensor.py | 4 +- tests/components/deconz/test_binary_sensor.py | 217 +++++++----- tests/components/deconz/test_climate.py | 319 +++++++++++------- tests/components/deconz/test_sensor.py | 283 ++++++++++------ 4 files changed, 520 insertions(+), 303 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 001721d4f00035..cc3f3de3170c66 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,5 +1,5 @@ """Support for deCONZ sensors.""" -from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch +from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch, Thermostat from homeassistant.const import ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY from homeassistant.core import callback @@ -48,7 +48,7 @@ def async_add_sensor(sensors): hass.async_create_task(new_event.async_update_device_registry()) gateway.events.append(new_event) - elif not sensor.BINARY: + elif not sensor.BINARY and sensor.type not in Thermostat.ZHATYPE: new_sensor = DeconzSensor(sensor, gateway) entity_handler.add_entity(new_sensor) diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index b6745e1a971312..c5c35f108296b0 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -1,79 +1,98 @@ """deCONZ binary sensor platform tests.""" -from unittest.mock import Mock, patch +from copy import deepcopy -from tests.common import mock_coro +from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.components.binary_sensor as binary_sensor -SENSOR = { +SENSORS = { "1": { - "id": "Sensor 1 id", - "name": "Sensor 1 name", + "id": "Presence sensor id", + "name": "Presence sensor", "type": "ZHAPresence", - "state": {"presence": False}, - "config": {}, + "state": {"dark": False, "presence": False}, + "config": {"on": True, "reachable": True, "temperature": 10}, "uniqueid": "00:00:00:00:00:00:00:00-00", }, "2": { - "id": "Sensor 2 id", - "name": "Sensor 2 name", + "id": "Temperature sensor id", + "name": "Temperature sensor", "type": "ZHATemperature", "state": {"temperature": False}, "config": {}, + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "3": { + "id": "CLIP presence sensor id", + "name": "CLIP presence sensor", + "type": "CLIPPresence", + "state": {}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, + "4": { + "id": "Vibration sensor id", + "name": "Vibration sensor", + "type": "ZHAVibration", + "state": { + "orientation": [1, 2, 3], + "tiltangle": 36, + "vibration": True, + "vibrationstrength": 10, + }, + "config": {"on": True, "reachable": True, "temperature": 10}, + "uniqueid": "00:00:00:00:00:00:00:03-00", }, } +BRIDGEID = "0123456789" ENTRY_CONFIG = { deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: "0123456789", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } -ENTRY_OPTIONS = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, } +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} -async def setup_gateway(hass, data, allow_clip_sensor=True): - """Load the deCONZ binary sensor platform.""" - from pydeconz import DeconzSession - - loop = Mock() - session = Mock() - - ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, system_options={}, - options=ENTRY_OPTIONS, + options=options, + entry_id="1", ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") - # To flush out the service call to update the group + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() - return gateway + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] async def test_platform_manually_configured(hass): @@ -89,58 +108,94 @@ async def test_platform_manually_configured(hass): async def test_no_binary_sensors(hass): """Test that no sensors in deconz results in no sensor entities.""" - data = {} - gateway = await setup_gateway(hass, data) - assert len(hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids) == 0 + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_binary_sensors(hass): """Test successful creation of binary sensor entities.""" - data = {"sensors": SENSOR} - gateway = await setup_gateway(hass, data) - assert "binary_sensor.sensor_1_name" in gateway.deconz_ids - assert "binary_sensor.sensor_2_name" not in gateway.deconz_ids - assert len(hass.states.async_all()) == 1 - - hass.data[deconz.DOMAIN][gateway.bridgeid].api.sensors["1"].async_update( - {"state": {"on": False}} + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) + assert "binary_sensor.presence_sensor" in gateway.deconz_ids + assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids + assert "binary_sensor.clip_presence_sensor" not in gateway.deconz_ids + assert "binary_sensor.vibration_sensor" in gateway.deconz_ids + assert len(hass.states.async_all()) == 3 + presence_sensor = hass.states.get("binary_sensor.presence_sensor") + assert presence_sensor.state == "off" -async def test_add_new_sensor(hass): - """Test successful creation of sensor entities.""" - data = {} - gateway = await setup_gateway(hass, data) - sensor = Mock() - sensor.name = "name" - sensor.type = "ZHAPresence" - sensor.BINARY = True - sensor.uniqueid = "1" - sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) - await hass.async_block_till_done() - assert "binary_sensor.name" in gateway.deconz_ids - - -async def test_do_not_allow_clip_sensor(hass): - """Test that clip sensors can be ignored.""" - data = {} - gateway = await setup_gateway(hass, data, allow_clip_sensor=False) - sensor = Mock() - sensor.name = "name" - sensor.type = "CLIPPresence" - sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) + temperature_sensor = hass.states.get("binary_sensor.temperature_sensor") + assert temperature_sensor is None + + clip_presence_sensor = hass.states.get("binary_sensor.clip_presence_sensor") + assert clip_presence_sensor is None + + vibration_sensor = hass.states.get("binary_sensor.vibration_sensor") + assert vibration_sensor.state == "on" + + gateway.api.sensors["1"].async_update({"state": {"presence": True}}) await hass.async_block_till_done() - assert len(gateway.deconz_ids) == 0 + presence_sensor = hass.states.get("binary_sensor.presence_sensor") + assert presence_sensor.state == "on" -async def test_unload_switch(hass): - """Test that it works to unload switch entities.""" - data = {"sensors": SENSOR} - gateway = await setup_gateway(hass, data) - await gateway.async_reset() +async def test_allow_clip_sensor(hass): + """Test that CLIP sensors can be allowed.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, + ENTRY_CONFIG, + options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}, + get_state_response=data, + ) + assert "binary_sensor.presence_sensor" in gateway.deconz_ids + assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids + assert "binary_sensor.clip_presence_sensor" in gateway.deconz_ids + assert "binary_sensor.vibration_sensor" in gateway.deconz_ids + assert len(hass.states.async_all()) == 4 - assert len(hass.states.async_all()) == 0 + presence_sensor = hass.states.get("binary_sensor.presence_sensor") + assert presence_sensor.state == "off" + + temperature_sensor = hass.states.get("binary_sensor.temperature_sensor") + assert temperature_sensor is None + + clip_presence_sensor = hass.states.get("binary_sensor.clip_presence_sensor") + assert clip_presence_sensor.state == "off" + + vibration_sensor = hass.states.get("binary_sensor.vibration_sensor") + assert vibration_sensor.state == "on" + + +async def test_add_new_binary_sensor(hass): + """Test that adding a new binary sensor works.""" + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 + + state_added = { + "t": "event", + "e": "added", + "r": "sensors", + "id": "1", + "sensor": deepcopy(SENSORS["1"]), + } + gateway.api.async_event_handler(state_added) + await hass.async_block_till_done() + + assert "binary_sensor.presence_sensor" in gateway.deconz_ids + + presence_sensor = hass.states.get("binary_sensor.presence_sensor") + assert presence_sensor.state == "off" diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index b76b3511a090bc..1211188d3db3fa 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,23 +1,18 @@ """deCONZ climate platform tests.""" from copy import deepcopy -from unittest.mock import Mock, patch -import asynctest +from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.components.climate as climate -from tests.common import mock_coro - - -SENSOR = { +SENSORS = { "1": { - "id": "Climate 1 id", - "name": "Climate 1 name", + "id": "Thermostat id", + "name": "Thermostat", "type": "ZHAThermostat", "state": {"on": True, "temperature": 2260, "valve": 30}, "config": { @@ -30,62 +25,66 @@ "uniqueid": "00:00:00:00:00:00:00:00-00", }, "2": { - "id": "Sensor 2 id", - "name": "Sensor 2 name", + "id": "Presence sensor id", + "name": "Presence sensor", "type": "ZHAPresence", "state": {"presence": False}, - "config": {}, + "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "3": { + "id": "CLIP thermostat id", + "name": "CLIP thermostat", + "type": "CLIPThermostat", + "state": {"on": True, "temperature": 2260, "valve": 30}, + "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", }, } +BRIDGEID = "0123456789" + ENTRY_CONFIG = { deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: "0123456789", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } -ENTRY_OPTIONS = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, } +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} -async def setup_gateway(hass, data, allow_clip_sensor=True): - """Load the deCONZ sensor platform.""" - from pydeconz import DeconzSession - - response = Mock( - status=200, json=asynctest.CoroutineMock(), text=asynctest.CoroutineMock() - ) - response.content_type = "application/json" - - session = Mock(put=asynctest.CoroutineMock(return_value=response)) - - ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, system_options={}, - options=ENTRY_OPTIONS, + options=options, + entry_id="1", ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(hass.loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - - await hass.config_entries.async_forward_entry_setup(config_entry, "climate") - # To flush out the service call to update the group + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() - return gateway + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] async def test_platform_manually_configured(hass): @@ -101,69 +100,155 @@ async def test_platform_manually_configured(hass): async def test_no_sensors(hass): """Test that no sensors in deconz results in no climate entities.""" - gateway = await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids - assert not hass.states.async_all() + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 + assert len(hass.states.async_all()) == 0 async def test_climate_devices(hass): """Test successful creation of sensor entities.""" - gateway = await setup_gateway(hass, {"sensors": deepcopy(SENSOR)}) - assert "climate.climate_1_name" in gateway.deconz_ids - assert "sensor.sensor_2_name" not in gateway.deconz_ids - assert len(hass.states.async_all()) == 1 - - gateway.api.sensors["1"].async_update({"state": {"on": False}}) - - await hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": "climate.climate_1_name", "hvac_mode": "auto"}, - blocking=True, - ) - gateway.api.session.put.assert_called_with( - "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"mode": "auto"}' + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) + assert "climate.thermostat" in gateway.deconz_ids + assert "sensor.thermostat" not in gateway.deconz_ids + assert "sensor.thermostat_battery_level" in gateway.deconz_ids + assert "climate.presence_sensor" not in gateway.deconz_ids + assert "climate.clip_thermostat" not in gateway.deconz_ids + assert len(hass.states.async_all()) == 3 + + thermostat = hass.states.get("climate.thermostat") + assert thermostat.state == "auto" - await hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": "climate.climate_1_name", "hvac_mode": "heat"}, - blocking=True, - ) - gateway.api.session.put.assert_called_with( - "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"mode": "heat"}' - ) + thermostat = hass.states.get("sensor.thermostat") + assert thermostat is None - await hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": "climate.climate_1_name", "hvac_mode": "off"}, - blocking=True, - ) - gateway.api.session.put.assert_called_with( - "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"mode": "off"}' - ) + thermostat_battery_level = hass.states.get("sensor.thermostat_battery_level") + assert thermostat_battery_level.state == "100" - await hass.services.async_call( - "climate", - "set_temperature", - {"entity_id": "climate.climate_1_name", "temperature": 20}, - blocking=True, - ) - gateway.api.session.put.assert_called_with( - "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"heatsetpoint": 2000.0}' + presence_sensor = hass.states.get("climate.presence_sensor") + assert presence_sensor is None + + clip_thermostat = hass.states.get("climate.clip_thermostat") + assert clip_thermostat is None + + thermostat_device = gateway.api.sensors["1"] + + thermostat_device.async_update({"config": {"mode": "off"}}) + await hass.async_block_till_done() + + thermostat = hass.states.get("climate.thermostat") + assert thermostat.state == "off" + + thermostat_device.async_update({"config": {"mode": "other"}, "state": {"on": True}}) + await hass.async_block_till_done() + + thermostat = hass.states.get("climate.thermostat") + assert thermostat.state == "heat" + + thermostat_device.async_update({"state": {"on": False}}) + await hass.async_block_till_done() + + thermostat = hass.states.get("climate.thermostat") + assert thermostat.state == "off" + + # Verify service calls + + with patch.object( + thermostat_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + climate.DOMAIN, + climate.SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.thermostat", "hvac_mode": "auto"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/sensors/1/config", {"mode": "auto"}) + + with patch.object( + thermostat_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + climate.DOMAIN, + climate.SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.thermostat", "hvac_mode": "heat"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/sensors/1/config", {"mode": "heat"}) + + with patch.object( + thermostat_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + climate.DOMAIN, + climate.SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.thermostat", "hvac_mode": "off"}, + blocking=True, + ) + set_callback.assert_called_with("/sensors/1/config", {"mode": "off"}) + + with patch.object( + thermostat_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + climate.DOMAIN, + climate.SERVICE_SET_TEMPERATURE, + {"entity_id": "climate.thermostat", "temperature": 20}, + blocking=True, + ) + set_callback.assert_called_with("/sensors/1/config", {"heatsetpoint": 2000.0}) + + +async def test_clip_climate_device(hass): + """Test successful creation of sensor entities.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, + ENTRY_CONFIG, + options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}, + get_state_response=data, ) + assert "climate.thermostat" in gateway.deconz_ids + assert "sensor.thermostat" not in gateway.deconz_ids + assert "sensor.thermostat_battery_level" in gateway.deconz_ids + assert "climate.presence_sensor" not in gateway.deconz_ids + assert "climate.clip_thermostat" in gateway.deconz_ids + assert len(hass.states.async_all()) == 4 + + thermostat = hass.states.get("climate.thermostat") + assert thermostat.state == "auto" + + thermostat = hass.states.get("sensor.thermostat") + assert thermostat is None + + thermostat_battery_level = hass.states.get("sensor.thermostat_battery_level") + assert thermostat_battery_level.state == "100" + + presence_sensor = hass.states.get("climate.presence_sensor") + assert presence_sensor is None - assert len(gateway.api.session.put.mock_calls) == 4 + clip_thermostat = hass.states.get("climate.clip_thermostat") + assert clip_thermostat.state == "heat" async def test_verify_state_update(hass): """Test that state update properly.""" - gateway = await setup_gateway(hass, {"sensors": deepcopy(SENSOR)}) - assert "climate.climate_1_name" in gateway.deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert "climate.thermostat" in gateway.deconz_ids - thermostat = hass.states.get("climate.climate_1_name") + thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "auto" state_update = { @@ -174,44 +259,32 @@ async def test_verify_state_update(hass): "state": {"on": False}, } gateway.api.async_event_handler(state_update) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 - thermostat = hass.states.get("climate.climate_1_name") + thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "auto" assert gateway.api.sensors["1"].changed_keys == {"state", "r", "t", "on", "e", "id"} async def test_add_new_climate_device(hass): - """Test successful creation of climate entities.""" - gateway = await setup_gateway(hass, {}) - sensor = Mock() - sensor.name = "name" - sensor.type = "ZHAThermostat" - sensor.uniqueid = "1" - sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) - await hass.async_block_till_done() - assert "climate.name" in gateway.deconz_ids - - -async def test_do_not_allow_clipsensor(hass): - """Test that clip sensors can be ignored.""" - gateway = await setup_gateway(hass, {}, allow_clip_sensor=False) - sensor = Mock() - sensor.name = "name" - sensor.type = "CLIPThermostat" - sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) - await hass.async_block_till_done() + """Test that adding a new climate device works.""" + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) assert len(gateway.deconz_ids) == 0 + state_added = { + "t": "event", + "e": "added", + "r": "sensors", + "id": "1", + "sensor": deepcopy(SENSORS["1"]), + } + gateway.api.async_event_handler(state_added) + await hass.async_block_till_done() -async def test_unload_sensor(hass): - """Test that it works to unload sensor entities.""" - gateway = await setup_gateway(hass, {"sensors": SENSOR}) - - await gateway.async_reset() + assert "climate.thermostat" in gateway.deconz_ids - assert len(hass.states.async_all()) == 0 + thermostat = hass.states.get("climate.thermostat") + assert thermostat.state == "auto" diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index eb391cc563d378..947c42e6949834 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -1,123 +1,125 @@ """deCONZ sensor platform tests.""" -from unittest.mock import Mock, patch +from copy import deepcopy -from tests.common import mock_coro +from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.components.sensor as sensor -SENSOR = { +SENSORS = { "1": { - "id": "Sensor 1 id", - "name": "Sensor 1 name", + "id": "Light sensor id", + "name": "Light level sensor", "type": "ZHALightLevel", "state": {"lightlevel": 30000, "dark": False}, - "config": {"reachable": True}, + "config": {"on": True, "reachable": True, "temperature": 10}, "uniqueid": "00:00:00:00:00:00:00:00-00", }, "2": { - "id": "Sensor 2 id", - "name": "Sensor 2 name", + "id": "Presence sensor id", + "name": "Presence sensor", "type": "ZHAPresence", "state": {"presence": False}, "config": {}, + "uniqueid": "00:00:00:00:00:00:00:01-00", }, "3": { - "id": "Sensor 3 id", - "name": "Sensor 3 name", + "id": "Switch 1 id", + "name": "Switch 1", "type": "ZHASwitch", "state": {"buttonevent": 1000}, "config": {}, + "uniqueid": "00:00:00:00:00:00:00:02-00", }, "4": { - "id": "Sensor 4 id", - "name": "Sensor 4 name", + "id": "Switch 2 id", + "name": "Switch 2", "type": "ZHASwitch", "state": {"buttonevent": 1000}, "config": {"battery": 100}, - "uniqueid": "00:00:00:00:00:00:00:01-00", + "uniqueid": "00:00:00:00:00:00:00:03-00", }, "5": { - "id": "Sensor 5 id", - "name": "Sensor 5 name", - "type": "ZHASwitch", - "state": {"buttonevent": 1000}, - "config": {"battery": 100}, - "uniqueid": "00:00:00:00:00:00:00:02:00-00", - }, - "6": { - "id": "Sensor 6 id", - "name": "Sensor 6 name", + "id": "Daylight sensor id", + "name": "Daylight sensor", "type": "Daylight", - "state": {"daylight": True}, + "state": {"daylight": True, "status": 130}, "config": {}, + "uniqueid": "00:00:00:00:00:00:00:04-00", }, - "7": { - "id": "Sensor 7 id", - "name": "Sensor 7 name", + "6": { + "id": "Power sensor id", + "name": "Power sensor", "type": "ZHAPower", "state": {"current": 2, "power": 6, "voltage": 3}, "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:05-00", }, - "8": { - "id": "Sensor 8 id", - "name": "Sensor 8 name", + "7": { + "id": "Consumption id", + "name": "Consumption sensor", "type": "ZHAConsumption", "state": {"consumption": 2, "power": 6}, "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:06-00", + }, + "8": { + "id": "CLIP light sensor id", + "name": "CLIP light level sensor", + "type": "CLIPLightLevel", + "state": {"lightlevel": 30000}, + "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:07-00", }, } +BRIDGEID = "0123456789" ENTRY_CONFIG = { deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: "0123456789", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } -ENTRY_OPTIONS = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, } +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} -async def setup_gateway(hass, data, allow_clip_sensor=True): - """Load the deCONZ sensor platform.""" - from pydeconz import DeconzSession - - loop = Mock() - session = Mock() - - ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, system_options={}, - options=ENTRY_OPTIONS, + options=options, + entry_id="1", ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - await hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - # To flush out the service call to update the group + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() - return gateway + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] async def test_platform_manually_configured(hass): @@ -133,56 +135,143 @@ async def test_platform_manually_configured(hass): async def test_no_sensors(hass): """Test that no sensors in deconz results in no sensor entities.""" - gateway = await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_sensors(hass): """Test successful creation of sensor entities.""" - gateway = await setup_gateway(hass, {"sensors": SENSOR}) - assert "sensor.sensor_1_name" in gateway.deconz_ids - assert "sensor.sensor_2_name" not in gateway.deconz_ids - assert "sensor.sensor_3_name" not in gateway.deconz_ids - assert "sensor.sensor_3_name_battery_level" not in gateway.deconz_ids - assert "sensor.sensor_4_name" not in gateway.deconz_ids - assert "sensor.sensor_4_name_battery_level" in gateway.deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert "sensor.light_level_sensor" in gateway.deconz_ids + assert "sensor.presence_sensor" not in gateway.deconz_ids + assert "sensor.switch_1" not in gateway.deconz_ids + assert "sensor.switch_1_battery_level" not in gateway.deconz_ids + assert "sensor.switch_2" not in gateway.deconz_ids + assert "sensor.switch_2_battery_level" in gateway.deconz_ids + assert "sensor.daylight_sensor" in gateway.deconz_ids + assert "sensor.power_sensor" in gateway.deconz_ids + assert "sensor.consumption_sensor" in gateway.deconz_ids + assert "sensor.clip_light_level_sensor" not in gateway.deconz_ids assert len(hass.states.async_all()) == 6 - gateway.api.sensors["1"].async_update({"state": {"on": False}}) - gateway.api.sensors["4"].async_update({"config": {"battery": 75}}) + light_level_sensor = hass.states.get("sensor.light_level_sensor") + assert light_level_sensor.state == "999.8" + presence_sensor = hass.states.get("sensor.presence_sensor") + assert presence_sensor is None -async def test_add_new_sensor(hass): - """Test successful creation of sensor entities.""" - gateway = await setup_gateway(hass, {}) - sensor = Mock() - sensor.name = "name" - sensor.type = "ZHATemperature" - sensor.uniqueid = "1" - sensor.BINARY = False - sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) - await hass.async_block_till_done() - assert "sensor.name" in gateway.deconz_ids + switch_1 = hass.states.get("sensor.switch_1") + assert switch_1 is None + + switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level") + assert switch_1_battery_level is None + switch_2 = hass.states.get("sensor.switch_2") + assert switch_2 is None -async def test_do_not_allow_clipsensor(hass): - """Test that clip sensors can be ignored.""" - gateway = await setup_gateway(hass, {}, allow_clip_sensor=False) - sensor = Mock() - sensor.name = "name" - sensor.type = "CLIPTemperature" - sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) + switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") + assert switch_2_battery_level.state == "100" + + daylight_sensor = hass.states.get("sensor.daylight_sensor") + assert daylight_sensor.state == "dawn" + + power_sensor = hass.states.get("sensor.power_sensor") + assert power_sensor.state == "6" + + consumption_sensor = hass.states.get("sensor.consumption_sensor") + assert consumption_sensor.state == "0.002" + + gateway.api.sensors["1"].async_update({"state": {"lightlevel": 2000}}) + gateway.api.sensors["4"].async_update({"config": {"battery": 75}}) await hass.async_block_till_done() - assert len(gateway.deconz_ids) == 0 + light_level_sensor = hass.states.get("sensor.light_level_sensor") + assert light_level_sensor.state == "1.6" -async def test_unload_sensor(hass): - """Test that it works to unload sensor entities.""" - gateway = await setup_gateway(hass, {"sensors": SENSOR}) + switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") + assert switch_2_battery_level.state == "75" - await gateway.async_reset() - assert len(hass.states.async_all()) == 0 +async def test_allow_clip_sensors(hass): + """Test that CLIP sensors can be allowed.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, + ENTRY_CONFIG, + options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}, + get_state_response=data, + ) + assert "sensor.light_level_sensor" in gateway.deconz_ids + assert "sensor.presence_sensor" not in gateway.deconz_ids + assert "sensor.switch_1" not in gateway.deconz_ids + assert "sensor.switch_1_battery_level" not in gateway.deconz_ids + assert "sensor.switch_2" not in gateway.deconz_ids + assert "sensor.switch_2_battery_level" in gateway.deconz_ids + assert "sensor.daylight_sensor" in gateway.deconz_ids + assert "sensor.power_sensor" in gateway.deconz_ids + assert "sensor.consumption_sensor" in gateway.deconz_ids + assert "sensor.clip_light_level_sensor" in gateway.deconz_ids + assert len(hass.states.async_all()) == 7 + + light_level_sensor = hass.states.get("sensor.light_level_sensor") + assert light_level_sensor.state == "999.8" + + presence_sensor = hass.states.get("sensor.presence_sensor") + assert presence_sensor is None + + switch_1 = hass.states.get("sensor.switch_1") + assert switch_1 is None + + switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level") + assert switch_1_battery_level is None + + switch_2 = hass.states.get("sensor.switch_2") + assert switch_2 is None + + switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") + assert switch_2_battery_level.state == "100" + + daylight_sensor = hass.states.get("sensor.daylight_sensor") + assert daylight_sensor.state == "dawn" + + power_sensor = hass.states.get("sensor.power_sensor") + assert power_sensor.state == "6" + + consumption_sensor = hass.states.get("sensor.consumption_sensor") + assert consumption_sensor.state == "0.002" + + clip_light_level_sensor = hass.states.get("sensor.clip_light_level_sensor") + assert clip_light_level_sensor.state == "999.8" + + +async def test_add_new_sensor(hass): + """Test that adding a new sensor works.""" + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 + + state_added = { + "t": "event", + "e": "added", + "r": "sensors", + "id": "1", + "sensor": deepcopy(SENSORS["1"]), + } + gateway.api.async_event_handler(state_added) + await hass.async_block_till_done() + + assert "sensor.light_level_sensor" in gateway.deconz_ids + + light_level_sensor = hass.states.get("sensor.light_level_sensor") + assert light_level_sensor.state == "999.8" From 873d331ee3e1408e673cb1c7bb6851e4b4e1ba1c Mon Sep 17 00:00:00 2001 From: roblandry Date: Wed, 18 Sep 2019 14:11:26 -0400 Subject: [PATCH 059/296] Fix torque degree char (#26183) * Fix for \xC2\xB0 char instead of degree symbol * Remove comment * Black --- homeassistant/components/torque/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 0806ba0799c452..10161856a47a71 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -88,7 +88,12 @@ def get(self, request): names[pid] = data[key] elif is_unit: pid = convert_pid(is_unit.group(1)) - units[pid] = data[key] + + temp_unit = data[key] + if "\\xC2\\xB0" in temp_unit: + temp_unit = temp_unit.replace("\\xC2\\xB0", "°") + + units[pid] = temp_unit elif is_value: pid = convert_pid(is_value.group(1)) if pid in self.sensors: From f66a42d521c3e465f7c8064d2cc432944386cf6b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Sep 2019 13:40:17 -0700 Subject: [PATCH 060/296] Updated frontend to 20190918.1 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 628099a47d2c15..978127c6342028 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190918.0" + "home-assistant-frontend==20190918.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bbeb228f3459e0..5eeec405e7da3d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190918.0 +home-assistant-frontend==20190918.1 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 415c8e514f58b8..9848716d895e32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190918.0 +home-assistant-frontend==20190918.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ff52cf2255c4c..69ca7eefe03a52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190918.0 +home-assistant-frontend==20190918.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From fccbaf38050a3678d9dbf5369ffe8382e2c0700e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 19 Sep 2019 00:32:15 +0000 Subject: [PATCH 061/296] [ci skip] Translation update --- .../ambiclimate/.translations/ru.json | 2 +- .../cert_expiry/.translations/lb.json | 7 +++++++ .../components/deconz/.translations/lb.json | 11 ++++++++++ .../components/life360/.translations/ru.json | 4 ++-- .../components/light/.translations/ca.json | 6 ++---- .../components/light/.translations/da.json | 4 ++-- .../components/light/.translations/de.json | 4 ++-- .../components/light/.translations/en.json | 2 -- .../components/light/.translations/es.json | 6 ++---- .../components/light/.translations/fr.json | 4 ++-- .../components/light/.translations/it.json | 4 ++-- .../components/light/.translations/ko.json | 4 ++-- .../components/light/.translations/lb.json | 4 ++-- .../components/light/.translations/nl.json | 8 +++---- .../components/light/.translations/no.json | 4 ++-- .../components/light/.translations/pl.json | 4 ++-- .../components/light/.translations/ru.json | 4 ++-- .../components/light/.translations/sl.json | 4 ++-- .../light/.translations/zh-Hant.json | 4 ++-- .../components/linky/.translations/lb.json | 3 +++ .../logi_circle/.translations/ru.json | 2 +- .../components/nest/.translations/ru.json | 2 +- .../components/point/.translations/ru.json | 2 +- .../solaredge/.translations/lb.json | 6 ++++-- .../components/switch/.translations/bg.json | 4 ++-- .../components/switch/.translations/ca.json | 6 ++---- .../components/switch/.translations/en.json | 2 -- .../components/switch/.translations/es.json | 2 -- .../components/switch/.translations/fr.json | 4 ++-- .../components/switch/.translations/it.json | 4 ++-- .../components/switch/.translations/ko.json | 6 ++++-- .../components/switch/.translations/lb.json | 6 ++++-- .../components/switch/.translations/nl.json | 6 ++++-- .../components/switch/.translations/no.json | 4 ++-- .../components/switch/.translations/pl.json | 4 ++-- .../components/switch/.translations/ru.json | 4 ++-- .../components/switch/.translations/sl.json | 4 ++-- .../switch/.translations/zh-Hant.json | 4 ++-- .../components/toon/.translations/ru.json | 2 +- .../components/traccar/.translations/lb.json | 18 ++++++++++++++++ .../twentemilieu/.translations/lb.json | 21 +++++++++++++++++++ .../components/unifi/.translations/nl.json | 6 ++++++ .../components/velbus/.translations/lb.json | 20 ++++++++++++++++++ .../components/vesync/.translations/lb.json | 3 +++ .../components/withings/.translations/ca.json | 3 +++ .../components/withings/.translations/ko.json | 3 +++ .../components/withings/.translations/lb.json | 3 ++- .../components/withings/.translations/nl.json | 3 +++ .../components/withings/.translations/ru.json | 3 +++ 49 files changed, 174 insertions(+), 76 deletions(-) create mode 100644 homeassistant/components/cert_expiry/.translations/lb.json create mode 100644 homeassistant/components/traccar/.translations/lb.json create mode 100644 homeassistant/components/twentemilieu/.translations/lb.json create mode 100644 homeassistant/components/velbus/.translations/lb.json diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json index 129579315a29de..a4300e1e5306c5 100644 --- a/homeassistant/components/ambiclimate/.translations/ru.json +++ b/homeassistant/components/ambiclimate/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.", "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "no_config": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/cert_expiry/.translations/lb.json b/homeassistant/components/cert_expiry/.translations/lb.json new file mode 100644 index 00000000000000..d6811728a22402 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/lb.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 41c75ec4aab242..c536e5771411f9 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -67,5 +67,16 @@ "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", "remote_gyro_activated": "Apparat ger\u00ebselt" } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ Clip Sensoren erlaben", + "allow_deconz_groups": "deCONZ Luucht Gruppen erlaben" + }, + "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index c03ad0f7e1f6a6..1e962142373f89 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -5,7 +5,7 @@ "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" }, "create_entry": { - "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 Life360]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "error": { "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", @@ -19,7 +19,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u041b\u043e\u0433\u0438\u043d" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 Life360]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", "title": "Life360" } }, diff --git a/homeassistant/components/light/.translations/ca.json b/homeassistant/components/light/.translations/ca.json index 4cdf3d9042cde2..c9b727088ab180 100644 --- a/homeassistant/components/light/.translations/ca.json +++ b/homeassistant/components/light/.translations/ca.json @@ -10,10 +10,8 @@ "is_on": "{name} est\u00e0 enc\u00e8s" }, "trigger_type": { - "turn_off": "{name} apagat", - "turn_on": "{name} enc\u00e8s", - "turned_off": "{entity_name} apagat", - "turned_on": "{entity_name} enc\u00e8s" + "turned_off": "{name} apagat", + "turned_on": "{name} enc\u00e8s" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json index 7b266ba74125c9..4ea4a94014ea83 100644 --- a/homeassistant/components/light/.translations/da.json +++ b/homeassistant/components/light/.translations/da.json @@ -1,8 +1,8 @@ { "device_automation": { "trigger_type": { - "turn_off": "{name} slukket", - "turn_on": "{name} t\u00e6ndt" + "turned_off": "{name} slukket", + "turned_on": "{name} t\u00e6ndt" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json index fcfc2773ed8941..2fe1c6b42dcd42 100644 --- a/homeassistant/components/light/.translations/de.json +++ b/homeassistant/components/light/.translations/de.json @@ -1,8 +1,8 @@ { "device_automation": { "trigger_type": { - "turn_off": "{name} ausgeschaltet", - "turn_on": "{name} eingeschaltet" + "turned_off": "{name} ausgeschaltet", + "turned_on": "{name} eingeschaltet" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/en.json b/homeassistant/components/light/.translations/en.json index 225d64be231ea6..3f37de5331e3ea 100644 --- a/homeassistant/components/light/.translations/en.json +++ b/homeassistant/components/light/.translations/en.json @@ -10,8 +10,6 @@ "is_on": "{entity_name} is on" }, "trigger_type": { - "turn_off": "{entity_name} turned off", - "turn_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off", "turned_on": "{entity_name} turned on" } diff --git a/homeassistant/components/light/.translations/es.json b/homeassistant/components/light/.translations/es.json index 533c4b3c0f3486..6bf91651d2e148 100644 --- a/homeassistant/components/light/.translations/es.json +++ b/homeassistant/components/light/.translations/es.json @@ -10,10 +10,8 @@ "is_on": "{entity_name} est\u00e1 encendida" }, "trigger_type": { - "turn_off": "{entity_name} apagada", - "turn_on": "{entity_name} encendida", - "turned_off": "{entity_name} apagado", - "turned_on": "{entity_name} encendido" + "turned_off": "{entity_name} apagada", + "turned_on": "{entity_name} encendida" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json index f0aabef2ae742d..fd30e9317180ec 100644 --- a/homeassistant/components/light/.translations/fr.json +++ b/homeassistant/components/light/.translations/fr.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} est allum\u00e9" }, "trigger_type": { - "turn_off": "{entity_name} d\u00e9sactiv\u00e9", - "turn_on": "{entity_name} activ\u00e9" + "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} activ\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/it.json b/homeassistant/components/light/.translations/it.json index 85a117f0b53f2b..2f4d2ca121f512 100644 --- a/homeassistant/components/light/.translations/it.json +++ b/homeassistant/components/light/.translations/it.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \u00e8 attivo" }, "trigger_type": { - "turn_off": "{entity_name} disattivato", - "turn_on": "{entity_name} attivato" + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ko.json b/homeassistant/components/light/.translations/ko.json index 7277ef5900f02a..e055f67421ef53 100644 --- a/homeassistant/components/light/.translations/ko.json +++ b/homeassistant/components/light/.translations/ko.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" }, "trigger_type": { - "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/lb.json b/homeassistant/components/light/.translations/lb.json index fdd76cda7e62fc..a7f807e8dcda54 100644 --- a/homeassistant/components/light/.translations/lb.json +++ b/homeassistant/components/light/.translations/lb.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} ass un" }, "trigger_type": { - "turn_off": "{entity_name} gouf ausgeschalt", - "turn_on": "{entity_name} gouf ugeschalt" + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/nl.json b/homeassistant/components/light/.translations/nl.json index 546fea78b6d5e9..63954ca83a9fa1 100644 --- a/homeassistant/components/light/.translations/nl.json +++ b/homeassistant/components/light/.translations/nl.json @@ -2,16 +2,16 @@ "device_automation": { "action_type": { "toggle": "Omschakelen {naam}", - "turn_off": "{Naam} uitschakelen", - "turn_on": "{Naam} inschakelen" + "turn_off": "{entity_name} uitschakelen", + "turn_on": "{entity_name} inschakelen" }, "condition_type": { "is_off": "{name} is uitgeschakeld", "is_on": "{name} is ingeschakeld" }, "trigger_type": { - "turn_off": "{name} is uitgeschakeld", - "turn_on": "{name} is ingeschakeld" + "turned_off": "{name} is uitgeschakeld", + "turned_on": "{name} is ingeschakeld" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/no.json b/homeassistant/components/light/.translations/no.json index 2241ca6644e9c0..008123739d9138 100644 --- a/homeassistant/components/light/.translations/no.json +++ b/homeassistant/components/light/.translations/no.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} er p\u00e5" }, "trigger_type": { - "turn_off": "{name} sl\u00e5tt av", - "turn_on": "{name} sl\u00e5tt p\u00e5" + "turned_off": "{name} sl\u00e5tt av", + "turned_on": "{name} sl\u00e5tt p\u00e5" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 9debeaf4169d4c..22a93909578608 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -10,8 +10,8 @@ "is_on": "(entity_name} jest w\u0142\u0105czony." }, "trigger_type": { - "turn_off": "{nazwa} wy\u0142\u0105czone", - "turn_on": "{name} w\u0142\u0105czone" + "turned_off": "{nazwa} wy\u0142\u0105czone", + "turned_on": "{name} w\u0142\u0105czone" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json index 3154e17a509c7f..ba9339c1a944e8 100644 --- a/homeassistant/components/light/.translations/ru.json +++ b/homeassistant/components/light/.translations/ru.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/sl.json b/homeassistant/components/light/.translations/sl.json index 432c8ae37d15fb..bef4f1583b6b19 100644 --- a/homeassistant/components/light/.translations/sl.json +++ b/homeassistant/components/light/.translations/sl.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} je vklopljen" }, "trigger_type": { - "turn_off": "{entity_name} izklopljen", - "turn_on": "{entity_name} vklopljen" + "turned_off": "{entity_name} izklopljen", + "turned_on": "{entity_name} vklopljen" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/zh-Hant.json b/homeassistant/components/light/.translations/zh-Hant.json index 8f5fec9b309e82..5ac06129463b35 100644 --- a/homeassistant/components/light/.translations/zh-Hant.json +++ b/homeassistant/components/light/.translations/zh-Hant.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \u5df2\u958b\u555f" }, "trigger_type": { - "turn_off": "{entity_name} \u5df2\u95dc\u9589", - "turn_on": "{entity_name} \u5df2\u958b\u555f" + "turned_off": "{entity_name} \u5df2\u95dc\u9589", + "turned_on": "{entity_name} \u5df2\u958b\u555f" } } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/lb.json b/homeassistant/components/linky/.translations/lb.json index d38002385590fa..cd3c7152c89e7d 100644 --- a/homeassistant/components/linky/.translations/lb.json +++ b/homeassistant/components/linky/.translations/lb.json @@ -4,6 +4,9 @@ "username_exists": "Kont ass scho konfigur\u00e9iert" }, "error": { + "access": "Keng Verbindung zu Enedis.fr, iwwerpr\u00e9ift d'Internet Verbindung", + "enedis": "Enedis.fr huet mat engem Feeler ge\u00e4ntwert: prob\u00e9iert sp\u00e9ider nach emol (normalerweis net t\u00ebscht 23h00 an 2h00)", + "unknown": "Onbekannte Feeler: prob\u00e9iert sp\u00e9ider nach emol (normalerweis net t\u00ebscht 23h00 an 2h00)", "username_exists": "Kont ass scho konfigur\u00e9iert", "wrong_login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert" }, diff --git a/homeassistant/components/logi_circle/.translations/ru.json b/homeassistant/components/logi_circle/.translations/ru.json index 1e9c089828fe92..40c7c8853daeb4 100644 --- a/homeassistant/components/logi_circle/.translations/ru.json +++ b/homeassistant/components/logi_circle/.translations/ru.json @@ -4,7 +4,7 @@ "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "external_error": "\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.", "external_setup": "Logi Circle \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.", - "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Logi Circle \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/logi_circle/)." + "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Logi Circle \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index 1c24acd96e427a..ac88ed224edb26 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -4,7 +4,7 @@ "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Nest \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)." + "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Nest \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430", diff --git a/homeassistant/components/point/.translations/ru.json b/homeassistant/components/point/.translations/ru.json index 2a10b234e99e21..487510969481f6 100644 --- a/homeassistant/components/point/.translations/ru.json +++ b/homeassistant/components/point/.translations/ru.json @@ -5,7 +5,7 @@ "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "external_setup": "Point \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.", - "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Point \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/point/)." + "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Point \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/point/)." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/solaredge/.translations/lb.json b/homeassistant/components/solaredge/.translations/lb.json index 957a0187c1d6de..afc558ca80cd84 100644 --- a/homeassistant/components/solaredge/.translations/lb.json +++ b/homeassistant/components/solaredge/.translations/lb.json @@ -10,8 +10,10 @@ "user": { "data": { "api_key": "API Schl\u00ebssel fir d\u00ebsen Site", - "name": "Numm vun d\u00ebser Installatioun" - } + "name": "Numm vun d\u00ebser Installatioun", + "site_id": "SolarEdge site-ID" + }, + "title": "API Parameter fir d\u00ebs Installatioun d\u00e9fin\u00e9ieren" } }, "title": "SolarEdge" diff --git a/homeassistant/components/switch/.translations/bg.json b/homeassistant/components/switch/.translations/bg.json index 31e41d3f504dea..efccc652d5beb7 100644 --- a/homeassistant/components/switch/.translations/bg.json +++ b/homeassistant/components/switch/.translations/bg.json @@ -10,8 +10,8 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" }, "trigger_type": { - "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", - "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" + "turned_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", + "turned_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ca.json b/homeassistant/components/switch/.translations/ca.json index 6fea704f756d49..dbf5e152656497 100644 --- a/homeassistant/components/switch/.translations/ca.json +++ b/homeassistant/components/switch/.translations/ca.json @@ -12,10 +12,8 @@ "turn_on": "{entity_name} activat" }, "trigger_type": { - "turn_off": "{entity_name} desactivat", - "turn_on": "{entity_name} activat", - "turned_off": "{entity_name} apagat", - "turned_on": "{entity_name} enc\u00e8s" + "turned_off": "{entity_name} desactivat", + "turned_on": "{entity_name} activat" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/en.json b/homeassistant/components/switch/.translations/en.json index ed036023755c40..391a071cb8f4fb 100644 --- a/homeassistant/components/switch/.translations/en.json +++ b/homeassistant/components/switch/.translations/en.json @@ -12,8 +12,6 @@ "turn_on": "{entity_name} turned on" }, "trigger_type": { - "turn_off": "{entity_name} turned off", - "turn_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off", "turned_on": "{entity_name} turned on" } diff --git a/homeassistant/components/switch/.translations/es.json b/homeassistant/components/switch/.translations/es.json index b38928fa753dce..24dbc2cdc1fff0 100644 --- a/homeassistant/components/switch/.translations/es.json +++ b/homeassistant/components/switch/.translations/es.json @@ -12,8 +12,6 @@ "turn_on": "{entity_name} encendido" }, "trigger_type": { - "turn_off": "{entity_name} apagado", - "turn_on": "{entity_name} encendido", "turned_off": "{entity_name} apagado", "turned_on": "{entity_name} encendido" } diff --git a/homeassistant/components/switch/.translations/fr.json b/homeassistant/components/switch/.translations/fr.json index eeffc9262e56ea..4775d62bce3758 100644 --- a/homeassistant/components/switch/.translations/fr.json +++ b/homeassistant/components/switch/.translations/fr.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} allum\u00e9" }, "trigger_type": { - "turn_off": "{entity_name} \u00e9teint", - "turn_on": "{entity_name} allum\u00e9" + "turned_off": "{entity_name} \u00e9teint", + "turned_on": "{entity_name} allum\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/it.json b/homeassistant/components/switch/.translations/it.json index c51ce8c6ee5934..254c09380c1d1d 100644 --- a/homeassistant/components/switch/.translations/it.json +++ b/homeassistant/components/switch/.translations/it.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} attivato" }, "trigger_type": { - "turn_off": "{entity_name} disattivato", - "turn_on": "{entity_name} attivato" + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ko.json b/homeassistant/components/switch/.translations/ko.json index 2156ea04e01d68..02c303f932987b 100644 --- a/homeassistant/components/switch/.translations/ko.json +++ b/homeassistant/components/switch/.translations/ko.json @@ -6,12 +6,14 @@ "turn_on": "{entity_name} \ucf1c\uae30" }, "condition_type": { + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" }, "trigger_type": { - "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/lb.json b/homeassistant/components/switch/.translations/lb.json index 291d8cb478f251..8e974a0a8de137 100644 --- a/homeassistant/components/switch/.translations/lb.json +++ b/homeassistant/components/switch/.translations/lb.json @@ -6,12 +6,14 @@ "turn_on": "{entity_name} uschalten" }, "condition_type": { + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un", "turn_off": "{entity_name} gouf ausgeschalt", "turn_on": "{entity_name} gouf ugeschalt" }, "trigger_type": { - "turn_off": "{entity_name} gouf ausgeschalt", - "turn_on": "{entity_name} gouf ugeschalt" + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/nl.json b/homeassistant/components/switch/.translations/nl.json index 1d8355d21581bf..5e2aa6747a4c32 100644 --- a/homeassistant/components/switch/.translations/nl.json +++ b/homeassistant/components/switch/.translations/nl.json @@ -6,12 +6,14 @@ "turn_on": "Zet {entity_name} aan." }, "condition_type": { + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld", "turn_off": "{entity_name} uitgeschakeld", "turn_on": "{entity_name} ingeschakeld" }, "trigger_type": { - "turn_off": "{entity_name} uitgeschakeld", - "turn_on": "{entity_name} ingeschakeld" + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/no.json b/homeassistant/components/switch/.translations/no.json index 8a00ac09549771..adc128991c5efc 100644 --- a/homeassistant/components/switch/.translations/no.json +++ b/homeassistant/components/switch/.translations/no.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} sl\u00e5tt p\u00e5" }, "trigger_type": { - "turn_off": "{entity_name} sl\u00e5tt av", - "turn_on": "{entity_name} sl\u00e5tt p\u00e5" + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index f564d1424eac61..c63799bf783923 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} w\u0142\u0105czone" }, "trigger_type": { - "turn_off": "{entity_name} wy\u0142\u0105czone", - "turn_on": "{entity_name} w\u0142\u0105czone" + "turned_off": "{entity_name} wy\u0142\u0105czone", + "turned_on": "{entity_name} w\u0142\u0105czone" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json index 1b0658cd174102..45a941b665de03 100644 --- a/homeassistant/components/switch/.translations/ru.json +++ b/homeassistant/components/switch/.translations/ru.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/sl.json b/homeassistant/components/switch/.translations/sl.json index 38edfe5a1953d7..89423e071fdc8b 100644 --- a/homeassistant/components/switch/.translations/sl.json +++ b/homeassistant/components/switch/.translations/sl.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} vklopljen" }, "trigger_type": { - "turn_off": "{entity_name} izklopljen", - "turn_on": "{entity_name} vklopljen" + "turned_off": "{entity_name} izklopljen", + "turned_on": "{entity_name} vklopljen" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/zh-Hant.json b/homeassistant/components/switch/.translations/zh-Hant.json index 0607f4ab08ee3c..c1a67897b16c84 100644 --- a/homeassistant/components/switch/.translations/zh-Hant.json +++ b/homeassistant/components/switch/.translations/zh-Hant.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} \u5df2\u958b\u555f" }, "trigger_type": { - "turn_off": "{entity_name} \u5df2\u95dc\u9589", - "turn_on": "{entity_name} \u5df2\u958b\u555f" + "turned_off": "{entity_name} \u5df2\u95dc\u9589", + "turned_on": "{entity_name} \u5df2\u958b\u555f" } } } \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json index 012aa65187c28b..0eddbe2a151d79 100644 --- a/homeassistant/components/toon/.translations/ru.json +++ b/homeassistant/components/toon/.translations/ru.json @@ -4,7 +4,7 @@ "client_id": "Client ID \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "client_secret": "Client secret \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", - "no_app": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Toon \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/toon/).", + "no_app": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Toon \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { diff --git a/homeassistant/components/traccar/.translations/lb.json b/homeassistant/components/traccar/.translations/lb.json new file mode 100644 index 00000000000000..8808d85a1d6db2 --- /dev/null +++ b/homeassistant/components/traccar/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Traccar Noriichten z'empf\u00e4nken.", + "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + }, + "create_entry": { + "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Traccar ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider Informatiounen." + }, + "step": { + "user": { + "description": "S\u00e9cher fir Traccar anzeriichten?", + "title": "Traccar ariichten" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/lb.json b/homeassistant/components/twentemilieu/.translations/lb.json new file mode 100644 index 00000000000000..0b07c5003ef2ff --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "address_exists": "Adresse ass scho ageriicht." + }, + "error": { + "connection_error": "Feeler beim verbannen." + }, + "step": { + "user": { + "data": { + "house_letter": "Haus Buschtaf/zous\u00e4tzlech", + "house_number": "Haus Nummer", + "post_code": "Postleitzuel" + }, + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json index f907364327c958..518f0066534411 100644 --- a/homeassistant/components/unifi/.translations/nl.json +++ b/homeassistant/components/unifi/.translations/nl.json @@ -32,6 +32,12 @@ "track_devices": "Netwerkapparaten volgen (Ubiquiti-apparaten)", "track_wired_clients": "Inclusief bedrade netwerkcli\u00ebnten" } + }, + "init": { + "data": { + "one": "Leeg", + "other": "Leeg" + } } } } diff --git a/homeassistant/components/velbus/.translations/lb.json b/homeassistant/components/velbus/.translations/lb.json new file mode 100644 index 00000000000000..89e0bd818d277c --- /dev/null +++ b/homeassistant/components/velbus/.translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "port_exists": "D\u00ebse Port ass scho konfigur\u00e9iert" + }, + "error": { + "connection_failed": "Feeler bei der velbus Verbindung", + "port_exists": "D\u00ebse Port ass scho konfigur\u00e9iert" + }, + "step": { + "user": { + "data": { + "name": "Numm fir d\u00ebs velbus Verbindung", + "port": "Verbindungs zeeche-folleg" + } + } + }, + "title": "Velbus Interface" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/lb.json b/homeassistant/components/vesync/.translations/lb.json index 7d1dbad19f3bd8..cfccd8b1dbb49a 100644 --- a/homeassistant/components/vesync/.translations/lb.json +++ b/homeassistant/components/vesync/.translations/lb.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_setup": "N\u00ebmmen eng eenzeg Instanz vu Vesync ass erlaabt." + }, "error": { "invalid_login": "Ong\u00ebltege Benotzernumm oder Passwuert" }, diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json index a96f8cff523fdd..2f2fdbe9b3f742 100644 --- a/homeassistant/components/withings/.translations/ca.json +++ b/homeassistant/components/withings/.translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Necessites configurar Withings abans de poder autenticar't-hi. Llegeix la documentaci\u00f3." + }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Withings per al perfil seleccionat." }, diff --git a/homeassistant/components/withings/.translations/ko.json b/homeassistant/components/withings/.translations/ko.json index 3c2f00ba4aef76..617964e0596aba 100644 --- a/homeassistant/components/withings/.translations/ko.json +++ b/homeassistant/components/withings/.translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Withings \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Withings \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/withings/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + }, "create_entry": { "default": "\uc120\ud0dd\ud55c \ud504\ub85c\ud544\ub85c Withings \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json index 994d02aa7f5936..9015f4908308d1 100644 --- a/homeassistant/components/withings/.translations/lb.json +++ b/homeassistant/components/withings/.translations/lb.json @@ -7,6 +7,7 @@ }, "title": "Benotzer Profil." } - } + }, + "title": "Withings" } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json index 1729879a154d8d..3776621bec208e 100644 --- a/homeassistant/components/withings/.translations/nl.json +++ b/homeassistant/components/withings/.translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "U moet Withings configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de documentatie te lezen]" + }, "create_entry": { "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel." }, diff --git a/homeassistant/components/withings/.translations/ru.json b/homeassistant/components/withings/.translations/ru.json index d9d5e14208f05b..c6c621fbdf33ee 100644 --- a/homeassistant/components/withings/.translations/ru.json +++ b/homeassistant/components/withings/.translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Withings \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, From 80136f3591b0580a38ac9ef2babf2f79fd444272 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 19 Sep 2019 09:39:09 +0300 Subject: [PATCH 062/296] Change datetime.now() to dt_util.now() (#26582) * Change datetime.now() to dt_util.now() in cases where the functionality should stay the same These changes should not affect the functionality, rather cleanup our codebase. In general we would like integrations to not to use datetime.now() unless there's a very good reason for it, rather use our own dt_util.now() which makes the code aware of our current time zone. * Use datetime.utcnow() for season sensor to get offset-naive utc time * Revert "Use datetime.utcnow() for season sensor to get offset-naive utc time" This reverts commit 5f36463d9c7d52f8e11ffcec7e57dfbc7b21bdd1. * BOM sensor last_updated should be UTC as well * Run black * Remove unused last_partition_update variable --- .../components/bmw_connected_drive/__init__.py | 4 ++-- homeassistant/components/bom/sensor.py | 13 ++++++++----- .../components/concord232/alarm_control_panel.py | 1 - .../components/concord232/binary_sensor.py | 7 ++++--- homeassistant/components/demo/weather.py | 5 +++-- homeassistant/components/dlna_dmr/media_player.py | 6 +++--- homeassistant/components/doorbird/camera.py | 3 ++- homeassistant/components/doorbird/switch.py | 5 +++-- homeassistant/components/ebusd/sensor.py | 5 ++--- homeassistant/components/ios/notify.py | 3 +-- homeassistant/components/mobile_app/notify.py | 3 +-- homeassistant/components/ring/light.py | 9 +++++---- homeassistant/components/ring/switch.py | 9 +++++---- homeassistant/components/season/sensor.py | 5 +++-- homeassistant/components/systemmonitor/sensor.py | 3 +-- homeassistant/components/upnp/sensor.py | 6 +++--- 16 files changed, 46 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 160c8a5e4551c9..8e67da86dc3014 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,5 +1,4 @@ """Reads vehicle status from BMW connected drive portal.""" -import datetime import logging import voluptuous as vol @@ -8,6 +7,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -100,7 +100,7 @@ def execute_service(call): # update every UPDATE_INTERVAL minutes, starting now # this should even out the load on the servers - now = datetime.datetime.now() + now = dt_util.utcnow() track_utc_time_change( hass, cd_account.update, diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index 33444f1099652a..ed22be003ad4fa 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -13,6 +13,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, @@ -240,7 +241,7 @@ def should_update(self): # Never updated before, therefore an update should occur. return True - now = datetime.datetime.now() + now = dt_util.utcnow() update_due_at = self.last_updated + datetime.timedelta(minutes=35) return now > update_due_at @@ -251,8 +252,8 @@ def update(self): _LOGGER.debug( "BOM was updated %s minutes ago, skipping update as" " < 35 minutes, Now: %s, LastUpdate: %s", - (datetime.datetime.now() - self.last_updated), - datetime.datetime.now(), + (dt_util.utcnow() - self.last_updated), + dt_util.utcnow(), self.last_updated, ) return @@ -263,8 +264,10 @@ def update(self): # set lastupdate using self._data[0] as the first element in the # array is the latest date in the json - self.last_updated = datetime.datetime.strptime( - str(self._data[0]["local_date_time_full"]), "%Y%m%d%H%M%S" + self.last_updated = dt_util.as_utc( + datetime.datetime.strptime( + str(self._data[0]["local_date_time_full"]), "%Y%m%d%H%M%S" + ) ) return diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index 68f0d77e307a82..e86ec02040e3b3 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -69,7 +69,6 @@ def __init__(self, url, name, code, mode): self._url = url self._alarm = concord232_client.Client(self._url) self._alarm.partitions = self._alarm.list_partitions() - self._alarm.last_partition_update = datetime.datetime.now() @property def name(self): diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 10643f134d70e8..1a406d743b7cfa 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -12,6 +12,7 @@ ) from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -53,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.debug("Initializing client") client = concord232_client.Client(f"http://{host}:{port}") client.zones = client.list_zones() - client.last_zone_update = datetime.datetime.now() + client.last_zone_update = dt_util.utcnow() except requests.exceptions.ConnectionError as ex: _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) @@ -128,11 +129,11 @@ def is_on(self): def update(self): """Get updated stats from API.""" - last_update = datetime.datetime.now() - self._client.last_zone_update + last_update = dt_util.utcnow() - self._client.last_zone_update _LOGGER.debug("Zone: %s ", self._zone) if last_update > datetime.timedelta(seconds=1): self._client.zones = self._client.list_zones() - self._client.last_zone_update = datetime.datetime.now() + self._client.last_zone_update = dt_util.utcnow() _LOGGER.debug("Updated from zone: %s", self._zone["name"]) if hasattr(self._client, "zones"): diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index b81a2193bb54ab..2253f261ad2cef 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -1,5 +1,5 @@ """Demo platform that offers fake meteorological data.""" -from datetime import datetime, timedelta +from datetime import timedelta from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -10,6 +10,7 @@ WeatherEntity, ) from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +import homeassistant.util.dt as dt_util CONDITION_CLASSES = { "cloudy": [], @@ -147,7 +148,7 @@ def attribution(self): @property def forecast(self): """Return the forecast.""" - reftime = datetime.now().replace(hour=16, minute=00) + reftime = dt_util.now().replace(hour=16, minute=00) forecast_data = [] for entry in self._forecast: diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index c7c488950cc747..5dd7ab7a88a74d 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -1,6 +1,5 @@ """Support for DLNA DMR (Device Media Renderer).""" import asyncio -from datetime import datetime from datetime import timedelta import functools import logging @@ -43,6 +42,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util from homeassistant.util import get_local_ip _LOGGER = logging.getLogger(__name__) @@ -241,14 +241,14 @@ async def async_update(self): return # do we need to (re-)subscribe? - now = datetime.now() + now = dt_util.utcnow() should_renew = ( self._subscription_renew_time and now >= self._subscription_renew_time ) if should_renew or not was_available and self._available: try: timeout = await self._device.async_subscribe_services() - self._subscription_renew_time = datetime.now() + timeout / 2 + self._subscription_renew_time = dt_util.utcnow() + timeout / 2 except (asyncio.TimeoutError, aiohttp.ClientError): self._available = False _LOGGER.debug("Could not (re)subscribe") diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index eaae3f1923636d..457c319d9e1654 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -8,6 +8,7 @@ from homeassistant.components.camera import Camera, SUPPORT_STREAM from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.util.dt as dt_util from . import DOMAIN as DOORBIRD_DOMAIN @@ -77,7 +78,7 @@ def name(self): async def async_camera_image(self): """Pull a still image from the camera.""" - now = datetime.datetime.now() + now = dt_util.utcnow() if self._last_image and now - self._last_update < self._interval: return self._last_image diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py index 643e006dfef0ed..7a0dfa82e76eef 100644 --- a/homeassistant/components/doorbird/switch.py +++ b/homeassistant/components/doorbird/switch.py @@ -3,6 +3,7 @@ import logging from homeassistant.components.switch import SwitchDevice +import homeassistant.util.dt as dt_util from . import DOMAIN as DOORBIRD_DOMAIN @@ -66,7 +67,7 @@ def turn_on(self, **kwargs): else: self._state = self._doorstation.device.energize_relay(self._relay) - now = datetime.datetime.now() + now = dt_util.utcnow() self._assume_off = now + self._time def turn_off(self, **kwargs): @@ -75,6 +76,6 @@ def turn_off(self, **kwargs): def update(self): """Wait for the correct amount of assumed time to pass.""" - if self._state and self._assume_off <= datetime.datetime.now(): + if self._state and self._assume_off <= dt_util.utcnow(): self._state = False self._assume_off = datetime.datetime.min diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index ac156e040d7332..4bc79e7bd39c19 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -3,6 +3,7 @@ import datetime from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util from .const import DOMAIN @@ -68,9 +69,7 @@ def device_state_attributes(self): if index < len(time_frame): parsed = datetime.datetime.strptime(time_frame[index], "%H:%M") parsed = parsed.replace( - datetime.datetime.now().year, - datetime.datetime.now().month, - datetime.datetime.now().day, + dt_util.now().year, dt_util.now().month, dt_util.now().day ) schedule[item[0]] = parsed.isoformat() return schedule diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index 83e4c089b9a541..ee74b369629b75 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -1,5 +1,4 @@ """Support for iOS push notifications.""" -from datetime import datetime, timezone import logging import requests @@ -25,7 +24,7 @@ def log_rate_limits(hass, target, resp, level=20): """Output rate limit log line at given level.""" rate_limits = resp["rateLimits"] resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"]) - resetsAtTime = resetsAt - datetime.now(timezone.utc) + resetsAtTime = resetsAt - dt_util.utcnow() rate_limit_msg = ( "iOS push notification rate limits for %s: " "%d sent, %d allowed, %d errors, " diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index d16ed23266a412..1e6a0517026255 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -1,6 +1,5 @@ """Support for mobile_app push notifications.""" import asyncio -from datetime import datetime, timezone import logging import async_timeout @@ -60,7 +59,7 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO): rate_limits = resp[ATTR_PUSH_RATE_LIMITS] resetsAt = rate_limits[ATTR_PUSH_RATE_LIMITS_RESETS_AT] - resetsAtTime = dt_util.parse_datetime(resetsAt) - datetime.now(timezone.utc) + resetsAtTime = dt_util.parse_datetime(resetsAt) - dt_util.utcnow() rate_limit_msg = ( "mobile_app push notification rate limits for %s: " "%d sent, %d allowed, %d errors, " diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 5805114252e09a..697be4d1579220 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -1,9 +1,10 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" import logging -from datetime import datetime, timedelta +from datetime import timedelta from homeassistant.components.light import Light from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +import homeassistant.util.dt as dt_util from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING @@ -41,7 +42,7 @@ def __init__(self, device): self._device = device self._unique_id = self._device.id self._light_on = False - self._no_updates_until = datetime.now() + self._no_updates_until = dt_util.utcnow() async def async_added_to_hass(self): """Register callbacks.""" @@ -77,7 +78,7 @@ def _set_light(self, new_state): """Update light state, and causes HASS to correctly update.""" self._device.lights = new_state self._light_on = new_state == ON_STATE - self._no_updates_until = datetime.now() + SKIP_UPDATES_DELAY + self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.async_schedule_update_ha_state(True) def turn_on(self, **kwargs): @@ -90,7 +91,7 @@ def turn_off(self, **kwargs): def update(self): """Update current state of the light.""" - if self._no_updates_until > datetime.now(): + if self._no_updates_until > dt_util.utcnow(): _LOGGER.debug("Skipping update...") return diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index cbbecb1a40398b..413d2a70aae2ba 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -1,9 +1,10 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" import logging -from datetime import datetime, timedelta +from datetime import timedelta from homeassistant.components.switch import SwitchDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +import homeassistant.util.dt as dt_util from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING @@ -72,14 +73,14 @@ class SirenSwitch(BaseRingSwitch): def __init__(self, device): """Initialize the switch for a device with a siren.""" super().__init__(device, "siren") - self._no_updates_until = datetime.now() + self._no_updates_until = dt_util.utcnow() self._siren_on = False def _set_switch(self, new_state): """Update switch state, and causes HASS to correctly update.""" self._device.siren = new_state self._siren_on = new_state > 0 - self._no_updates_until = datetime.now() + SKIP_UPDATES_DELAY + self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.schedule_update_ha_state() @property @@ -102,7 +103,7 @@ def icon(self): def update(self): """Update state of the siren.""" - if self._no_updates_until > datetime.now(): + if self._no_updates_until > dt_util.utcnow(): _LOGGER.debug("Skipping update...") return self._siren_on = self._device.siren > 0 diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index f2e0ccac2b0008..cdd6af57617e46 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -8,6 +8,7 @@ from homeassistant.const import CONF_TYPE from homeassistant.helpers.entity import Entity from homeassistant import util +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -104,7 +105,7 @@ def __init__(self, hass, hemisphere, season_tracking_type): """Initialize the season.""" self.hass = hass self.hemisphere = hemisphere - self.datetime = datetime.now() + self.datetime = dt_util.utcnow().replace(tzinfo=None) self.type = season_tracking_type self.season = get_season(self.datetime, self.hemisphere, self.type) @@ -125,5 +126,5 @@ def icon(self): def update(self): """Update season.""" - self.datetime = datetime.utcnow() + self.datetime = dt_util.utcnow().replace(tzinfo=None) self.season = get_season(self.datetime, self.hemisphere, self.type) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 446a36ec350f88..ad2072baaa5230 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -1,5 +1,4 @@ """Support for monitoring the local system.""" -from datetime import datetime import logging import os import socket @@ -193,7 +192,7 @@ def update(self): counters = psutil.net_io_counters(pernic=True) if self.argument in counters: counter = counters[self.argument][IO_COUNTER[self.type]] - now = datetime.now() + now = dt_util.utcnow() if self._last_value and self._last_value < counter: self._state = round( (counter - self._last_value) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index e5746e088f866b..b721fa29cdd860 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -1,11 +1,11 @@ """Support for UPnP/IGD Sensors.""" -from datetime import datetime import logging from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.dt as dt_util from .const import DOMAIN as DOMAIN_UPNP, SIGNAL_REMOVE_SENSOR @@ -199,10 +199,10 @@ async def async_update(self): if self._last_value is None: self._last_value = new_value - self._last_update_time = datetime.now() + self._last_update_time = dt_util.utcnow() return - now = datetime.now() + now = dt_util.utcnow() if self._is_overflowed(new_value): self._state = None # temporarily report nothing else: From 5e15675593ba94a2c11f9f929cdad317e27ce190 Mon Sep 17 00:00:00 2001 From: Martin Brooksbank Date: Thu, 19 Sep 2019 08:55:07 +0100 Subject: [PATCH 063/296] Add additional needles to glances cpu_temp attribute (#22311) * Added additional needles to the cpu_temp attribute * Fix conflict --- homeassistant/components/glances/sensor.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 1385d5e59a768b..90b4b386f37e52 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -197,18 +197,20 @@ async def async_update(self): elif self.type == "cpu_temp": for sensor in value["sensors"]: if sensor["label"] in [ - "CPU", + "amdgpu 1", + "aml_thermal", + "Core 0", + "Core 1", "CPU Temperature", - "Package id 0", - "Physical id 0", - "cpu_thermal 1", + "CPU", "cpu-thermal 1", + "cpu_thermal 1", "exynos-therm 1", - "soc_thermal 1", + "Package id 0", + "Physical id 0", + "radeon 1", "soc-thermal 1", - "aml_thermal", - "Core 0", - "Core 1", + "soc_thermal 1", ]: self._state = sensor["value"] elif self.type == "docker_active": From 770eeaf82f4800127e977f980f5438d0fa8edbc9 Mon Sep 17 00:00:00 2001 From: Andrew Rowson Date: Thu, 19 Sep 2019 11:51:49 +0100 Subject: [PATCH 064/296] Encode prometheus metric names per the prom spec (#26639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Referencing issue #26418. Prometheus metric names can only contain chars a-zA-Z0-9, : and _ (https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels). HA currently generates invalid prometheus names, e.g. if the unit for a sensor is a non-ASCII character containing ° or μ. To resolve, we need to sanitize the name before creating, replacing non-valid characters with a valid representation. In this case, I've used "u{unicode-hex-code}". Also updated the test case to make sure that the ° case is handled. --- homeassistant/components/prometheus/__init__.py | 16 +++++++++++++++- tests/components/prometheus/test_init.py | 11 +++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 1ba2c4809b6618..82db5f6725f512 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -1,5 +1,6 @@ """Support for Prometheus metrics export.""" import logging +import string from aiohttp import web import voluptuous as vol @@ -159,10 +160,23 @@ def _metric(self, metric, factory, documentation, labels=None): try: return self._metrics[metric] except KeyError: - full_metric_name = f"{self.metrics_prefix}{metric}" + full_metric_name = self._sanitize_metric_name( + f"{self.metrics_prefix}{metric}" + ) self._metrics[metric] = factory(full_metric_name, documentation, labels) return self._metrics[metric] + @staticmethod + def _sanitize_metric_name(metric: str) -> str: + return "".join( + [ + c + if c in string.ascii_letters or c.isdigit() or c == "_" or c == ":" + else f"u{hex(ord(c))}" + for c in metric + ] + ) + @staticmethod def state_as_number(state): """Return a state casted to a float.""" diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 9e313fd3694583..4ec40731c5d206 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -41,6 +41,11 @@ async def prometheus_client(loop, hass, hass_client): sensor3.entity_id = "sensor.electricity_price" await sensor3.async_update_ha_state() + sensor4 = DemoSensor("Wind Direction", 25, None, "°", None) + sensor4.hass = hass + sensor4.entity_id = "sensor.wind_direction" + await sensor4.async_update_ha_state() + return await hass_client() @@ -103,3 +108,9 @@ def test_view(prometheus_client): # pylint: disable=redefined-outer-name 'entity="sensor.electricity_price",' 'friendly_name="Electricity price"} 0.123' in body ) + + assert ( + 'sensor_unit_u0xb0{domain="sensor",' + 'entity="sensor.wind_direction",' + 'friendly_name="Wind Direction"} 25.0' in body + ) From 1041b106168fe00934c721c1764d53de912808d9 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 19 Sep 2019 19:19:27 +0300 Subject: [PATCH 065/296] Move alexa integration to use dt_util (#26723) --- homeassistant/components/alexa/capabilities.py | 4 ++-- homeassistant/components/alexa/flash_briefings.py | 4 ++-- homeassistant/components/alexa/handlers.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index d769f797da1bfa..aeaa0a62c4bdbb 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,5 +1,4 @@ """Alexa capabilities.""" -from datetime import datetime import logging from homeassistant.const import ( @@ -16,6 +15,7 @@ import homeassistant.components.climate.const as climate from homeassistant.components import light, fan, cover import homeassistant.util.color as color_util +import homeassistant.util.dt as dt_util from .const import ( API_TEMP_UNITS, @@ -109,7 +109,7 @@ def serialize_properties(self): "name": prop_name, "namespace": self.name(), "value": prop_value, - "timeOfSample": datetime.now().strftime(DATE_FORMAT), + "timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT), "uncertaintyInMilliseconds": 0, } diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 708d1592e4c0fb..0b5c1243764ce2 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -1,9 +1,9 @@ """Support for Alexa skill service end point.""" import copy -from datetime import datetime import logging import uuid +import homeassistant.util.dt as dt_util from homeassistant.components import http from homeassistant.core import callback from homeassistant.helpers import template @@ -89,7 +89,7 @@ def get(self, request, briefing_id): else: output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL) - output[ATTR_UPDATE_DATE] = datetime.now().strftime(DATE_FORMAT) + output[ATTR_UPDATE_DATE] = dt_util.utcnow().strftime(DATE_FORMAT) briefing.append(output) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 1e636b96ee5205..c72101460c4819 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1,5 +1,4 @@ """Alexa message handlers.""" -from datetime import datetime import logging import math @@ -28,6 +27,7 @@ TEMP_FAHRENHEIT, ) import homeassistant.util.color as color_util +import homeassistant.util.dt as dt_util from homeassistant.util.decorator import Registry from homeassistant.util.temperature import convert as convert_temperature @@ -275,7 +275,7 @@ async def async_api_activate(hass, config, directive, context): payload = { "cause": {"type": Cause.VOICE_INTERACTION}, - "timestamp": "%sZ" % (datetime.utcnow().isoformat(),), + "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", } return directive.response( @@ -299,7 +299,7 @@ async def async_api_deactivate(hass, config, directive, context): payload = { "cause": {"type": Cause.VOICE_INTERACTION}, - "timestamp": "%sZ" % (datetime.utcnow().isoformat(),), + "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", } return directive.response( From 468deef32674b40a140886ef3b8ecbc370611004 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Sep 2019 20:02:21 +0200 Subject: [PATCH 066/296] Bumps pytest to 5.1.2 (#26718) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index bfe459b0cfb8d3..0d96492e3aadbe 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -17,5 +17,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.1.1 +pytest==5.1.2 requests_mock==1.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69ca7eefe03a52..87f02e53b79677 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -18,7 +18,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.1.1 +pytest==5.1.2 requests_mock==1.6.0 From a8a485abf7ba95fe6b744c24cf80cf45453ca09b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Sep 2019 20:34:41 +0200 Subject: [PATCH 067/296] Bumps aiohttp to 3.6.0 (#26728) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- .../smartthings/test_config_flow.py | 25 ++++++++++++--- tests/components/smartthings/test_init.py | 31 ++++++++++++++----- tests/test_util/aiohttp.py | 6 +++- 6 files changed, 51 insertions(+), 17 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5eeec405e7da3d..0a5cfbfc67d15a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiohttp==3.5.4 +aiohttp==3.6.0 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9848716d895e32..a00441518a54fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.5.4 +aiohttp==3.6.0 astral==1.10.1 async_timeout==3.0.1 attrs==19.1.0 diff --git a/setup.py b/setup.py index 5ab8d74c64cf00..8be458fc449c16 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - "aiohttp==3.5.4", + "aiohttp==3.6.0", "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.1.0", diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 5724d7a3bac0be..fce0129a7bf399 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -84,7 +84,10 @@ async def test_token_unauthorized(hass, smartthings_mock): flow = SmartThingsFlowHandler() flow.hass = hass - smartthings_mock.apps.side_effect = ClientResponseError(None, None, status=401) + request_info = Mock(real_url="http://example.com") + smartthings_mock.apps.side_effect = ClientResponseError( + request_info=request_info, history=None, status=401 + ) result = await flow.async_step_user({"access_token": str(uuid4())}) @@ -98,7 +101,10 @@ async def test_token_forbidden(hass, smartthings_mock): flow = SmartThingsFlowHandler() flow.hass = hass - smartthings_mock.apps.side_effect = ClientResponseError(None, None, status=403) + request_info = Mock(real_url="http://example.com") + smartthings_mock.apps.side_effect = ClientResponseError( + request_info=request_info, history=None, status=403 + ) result = await flow.async_step_user({"access_token": str(uuid4())}) @@ -113,7 +119,10 @@ async def test_webhook_error(hass, smartthings_mock): flow.hass = hass data = {"error": {}} - error = APIResponseError(None, None, data=data, status=422) + request_info = Mock(real_url="http://example.com") + error = APIResponseError( + request_info=request_info, history=None, data=data, status=422 + ) error.is_target_error = Mock(return_value=True) smartthings_mock.apps.side_effect = error @@ -131,7 +140,10 @@ async def test_api_error(hass, smartthings_mock): flow.hass = hass data = {"error": {}} - error = APIResponseError(None, None, data=data, status=400) + request_info = Mock(real_url="http://example.com") + error = APIResponseError( + request_info=request_info, history=None, data=data, status=400 + ) smartthings_mock.apps.side_effect = error @@ -147,7 +159,10 @@ async def test_unknown_api_error(hass, smartthings_mock): flow = SmartThingsFlowHandler() flow.hass = hass - smartthings_mock.apps.side_effect = ClientResponseError(None, None, status=404) + request_info = Mock(real_url="http://example.com") + smartthings_mock.apps.side_effect = ClientResponseError( + request_info=request_info, history=None, status=404 + ) result = await flow.async_step_user({"access_token": str(uuid4())}) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 4e1ffce7e22119..9749ab9bb71e3d 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -54,7 +54,10 @@ async def test_unrecoverable_api_errors_create_new_flow( """ assert await async_setup_component(hass, "persistent_notification", {}) config_entry.add_to_hass(hass) - smartthings_mock.app.side_effect = ClientResponseError(None, None, status=401) + request_info = Mock(real_url="http://example.com") + smartthings_mock.app.side_effect = ClientResponseError( + request_info=request_info, history=None, status=401 + ) # Assert setup returns false result = await smartthings.async_setup_entry(hass, config_entry) @@ -75,7 +78,10 @@ async def test_recoverable_api_errors_raise_not_ready( ): """Test config entry not ready raised for recoverable API errors.""" config_entry.add_to_hass(hass) - smartthings_mock.app.side_effect = ClientResponseError(None, None, status=500) + request_info = Mock(real_url="http://example.com") + smartthings_mock.app.side_effect = ClientResponseError( + request_info=request_info, history=None, status=500 + ) with pytest.raises(ConfigEntryNotReady): await smartthings.async_setup_entry(hass, config_entry) @@ -86,9 +92,12 @@ async def test_scenes_api_errors_raise_not_ready( ): """Test if scenes are unauthorized we continue to load platforms.""" config_entry.add_to_hass(hass) + request_info = Mock(real_url="http://example.com") smartthings_mock.app.return_value = app smartthings_mock.installed_app.return_value = installed_app - smartthings_mock.scenes.side_effect = ClientResponseError(None, None, status=500) + smartthings_mock.scenes.side_effect = ClientResponseError( + request_info=request_info, history=None, status=500 + ) with pytest.raises(ConfigEntryNotReady): await smartthings.async_setup_entry(hass, config_entry) @@ -140,10 +149,13 @@ async def test_scenes_unauthorized_loads_platforms( ): """Test if scenes are unauthorized we continue to load platforms.""" config_entry.add_to_hass(hass) + request_info = Mock(real_url="http://example.com") smartthings_mock.app.return_value = app smartthings_mock.installed_app.return_value = installed_app smartthings_mock.devices.return_value = [device] - smartthings_mock.scenes.side_effect = ClientResponseError(None, None, status=403) + smartthings_mock.scenes.side_effect = ClientResponseError( + request_info=request_info, history=None, status=403 + ) mock_token = Mock() mock_token.access_token.return_value = str(uuid4()) mock_token.refresh_token.return_value = str(uuid4()) @@ -290,12 +302,13 @@ async def test_remove_entry_app_in_use(hass, config_entry, smartthings_mock): async def test_remove_entry_already_deleted(hass, config_entry, smartthings_mock): """Test handles when the apps have already been removed.""" + request_info = Mock(real_url="http://example.com") # Arrange smartthings_mock.delete_installed_app.side_effect = ClientResponseError( - None, None, status=403 + request_info=request_info, history=None, status=403 ) smartthings_mock.delete_app.side_effect = ClientResponseError( - None, None, status=403 + request_info=request_info, history=None, status=403 ) # Act await smartthings.async_remove_entry(hass, config_entry) @@ -308,9 +321,10 @@ async def test_remove_entry_installedapp_api_error( hass, config_entry, smartthings_mock ): """Test raises exceptions removing the installed app.""" + request_info = Mock(real_url="http://example.com") # Arrange smartthings_mock.delete_installed_app.side_effect = ClientResponseError( - None, None, status=500 + request_info=request_info, history=None, status=500 ) # Act with pytest.raises(ClientResponseError): @@ -337,8 +351,9 @@ async def test_remove_entry_installedapp_unknown_error( async def test_remove_entry_app_api_error(hass, config_entry, smartthings_mock): """Test raises exceptions removing the app.""" # Arrange + request_info = Mock(real_url="http://example.com") smartthings_mock.delete_app.side_effect = ClientResponseError( - None, None, status=500 + request_info=request_info, history=None, status=500 ) # Act with pytest.raises(ClientResponseError): diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 98fc70f3bf51f3..ce13ca5a594aa5 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -244,8 +244,12 @@ def release(self): def raise_for_status(self): """Raise error if status is 400 or higher.""" if self.status >= 400: + request_info = mock.Mock(real_url="http://example.com") raise ClientResponseError( - None, None, code=self.status, headers=self.headers + request_info=request_info, + history=None, + code=self.status, + headers=self.headers, ) def close(self): From 1e5de9e9e4e9806d7a0b2a3b691b225bb379a12e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Sep 2019 11:49:47 -0700 Subject: [PATCH 068/296] Bump TRADFRI (#26731) * Bump TRADFRI * Fix test --- homeassistant/components/tradfri/manifest.json | 12 +++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tradfri/test_light.py | 4 +++- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index ba6b21e00283ab..d847c6df24f7cb 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,17 +3,11 @@ "name": "Tradfri", "config_flow": true, "documentation": "https://www.home-assistant.io/components/tradfri", - "requirements": [ - "pytradfri[async]==6.0.1" - ], + "requirements": ["pytradfri[async]==6.3.1"], "homekit": { - "models": [ - "TRADFRI" - ] + "models": ["TRADFRI"] }, "dependencies": [], "zeroconf": ["_coap._udp.local."], - "codeowners": [ - "@ggravlingen" - ] + "codeowners": ["@ggravlingen"] } diff --git a/requirements_all.txt b/requirements_all.txt index a00441518a54fc..99e94ec438abc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1596,7 +1596,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==6.0.1 +pytradfri[async]==6.3.1 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87f02e53b79677..0ac705bf2e1d72 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ python-velbus==2.0.27 python_awair==0.0.4 # homeassistant.components.tradfri -pytradfri[async]==6.0.1 +pytradfri[async]==6.3.1 # homeassistant.components.vesync pyvesync==1.1.0 diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 69fe3bae618dab..4c691f66af872c 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -4,7 +4,9 @@ from unittest.mock import Mock, MagicMock, patch, PropertyMock import pytest -from pytradfri.device import Device, LightControl, Light +from pytradfri.device import Device +from pytradfri.device.light import Light +from pytradfri.device.light_control import LightControl from homeassistant.components import tradfri From 44cde5fb733f0ef8d7a992919946e8a9586291df Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Sep 2019 20:50:45 +0200 Subject: [PATCH 069/296] Bumps pre-commit to 1.18.3 (#26717) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 0d96492e3aadbe..44b27d8e13e9c5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 mypy==0.720 -pre-commit==1.18.2 +pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 pytest-aiohttp==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ac705bf2e1d72..83ec2d1d2c1023 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -11,7 +11,7 @@ flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 mypy==0.720 -pre-commit==1.18.2 +pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 pytest-aiohttp==0.3.0 From 0e201fd85936339208d0e10d4ca22db7c9e04fc6 Mon Sep 17 00:00:00 2001 From: Robin Wohlers-Reichel Date: Fri, 20 Sep 2019 04:52:15 +1000 Subject: [PATCH 070/296] Update Solax Library to 0.2.2 (#26705) * bump version and adjust * Address review comments * Fix import order * bump solax version * Trigger Azure * default port --- homeassistant/components/solax/manifest.json | 2 +- homeassistant/components/solax/sensor.py | 28 +++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 52e50ab47998a5..3a154b857fe878 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -3,7 +3,7 @@ "name": "Solax Inverter", "documentation": "https://www.home-assistant.io/components/solax", "requirements": [ - "solax==0.1.2" + "solax==0.2.2" ], "dependencies": [], "codeowners": ["@squishykid"] diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 0c1cfcf21da32a..a5b4547b344894 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -4,9 +4,11 @@ from datetime import timedelta import logging +from solax import real_time_api +from solax.inverter import InverterError import voluptuous as vol -from homeassistant.const import TEMP_CELSIUS, CONF_IP_ADDRESS +from homeassistant.const import TEMP_CELSIUS, CONF_IP_ADDRESS, CONF_PORT from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -15,24 +17,28 @@ _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_IP_ADDRESS): cv.string}) +DEFAULT_PORT = 80 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) SCAN_INTERVAL = timedelta(seconds=30) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Platform setup.""" - import solax - - api = solax.RealTimeAPI(config[CONF_IP_ADDRESS]) + api = await real_time_api(config[CONF_IP_ADDRESS], config[CONF_PORT]) endpoint = RealTimeDataEndpoint(hass, api) resp = await api.get_data() serial = resp.serial_number hass.async_add_job(endpoint.async_refresh) async_track_time_interval(hass, endpoint.async_refresh, SCAN_INTERVAL) devices = [] - for sensor in solax.INVERTER_SENSORS: - idx, unit = solax.INVERTER_SENSORS[sensor] + for sensor, (idx, unit) in api.inverter.sensor_map().items(): if unit == "C": unit = TEMP_CELSIUS uid = f"{serial}-{idx}" @@ -56,16 +62,14 @@ async def async_refresh(self, now=None): This is the only method that should fetch new data for Home Assistant. """ - from solax import SolaxRequestError - try: api_response = await self.api.get_data() self.ready.set() - except SolaxRequestError: + except InverterError: if now is not None: self.ready.clear() - else: - raise PlatformNotReady + return + raise PlatformNotReady data = api_response.data for sensor in self.sensors: if sensor.key in data: diff --git a/requirements_all.txt b/requirements_all.txt index 99e94ec438abc4..e6ac06e9199fc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1785,7 +1785,7 @@ solaredge-local==0.1.4 solaredge==0.0.2 # homeassistant.components.solax -solax==0.1.2 +solax==0.2.2 # homeassistant.components.honeywell somecomfort==0.5.2 From 94192ecd7d7d60c9b4517d4dd0efd3f2ee770d3d Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Thu, 19 Sep 2019 13:27:18 -0700 Subject: [PATCH 071/296] Bump pyobihai to fix issue with user account (#26736) --- homeassistant/components/obihai/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index b6bad10d608096..e7706b0435ceec 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -3,7 +3,7 @@ "name": "Obihai", "documentation": "https://www.home-assistant.io/components/obihai", "requirements": [ - "pyobihai==1.0.2" + "pyobihai==1.1.0" ], "dependencies": [], "codeowners": ["@dshokouhi"] diff --git a/requirements_all.txt b/requirements_all.txt index e6ac06e9199fc5..e67a7a00c79d88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1351,7 +1351,7 @@ pynx584==0.4 pynzbgetapi==0.2.0 # homeassistant.components.obihai -pyobihai==1.0.2 +pyobihai==1.1.0 # homeassistant.components.openuv pyopenuv==1.0.9 From 246a611a7c7c39c3f386a1c10daa14f513429362 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Sep 2019 23:05:02 +0200 Subject: [PATCH 072/296] Bump aiohttp to 3.6.1 (#26739) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0a5cfbfc67d15a..746485f2ece2da 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiohttp==3.6.0 +aiohttp==3.6.1 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index e67a7a00c79d88..8cbf9230ec0fc6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.6.0 +aiohttp==3.6.1 astral==1.10.1 async_timeout==3.0.1 attrs==19.1.0 diff --git a/setup.py b/setup.py index 8be458fc449c16..e6776d8a1a0811 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - "aiohttp==3.6.0", + "aiohttp==3.6.1", "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.1.0", From 2d12bac0e239c654077d8ab02c51f0a455703624 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 19 Sep 2019 16:29:26 -0500 Subject: [PATCH 073/296] Add Plex config flow support (#26548) * Add config flow support * Log error on failed connection * Review comments * Unused errors * Move form to step * Use instance var instead of passing argument * Only share servers created by component * Return errors early to avoid try:else * Separate debug for validation vs setup * Unnecessary * Unnecessary checks * Combine import flows, move logic to component * Use config entry discovery handler * Temporary lint fix * Filter out servers already configured * Remove manual config flow * Skip discovery if a config exists * Swap conditional to reduce indenting * Only discover when no configs created or creating * Un-nest function * Proper async use * Move legacy file import to discovery * Fix, bad else * Separate validate step * Unused without manual setup step * Async oops * First attempt at tests * Test cleanup * Full test coverage for config_flow, enable tests * Lint * Fix lint vs black * Add test init * Add test package requirement * Actually run script * Use 'not None' convention * Group exceptions by result * Improve logic, add new error and test * Test cleanup * Add more asserts --- .coveragerc | 5 +- .../components/discovery/__init__.py | 2 +- homeassistant/components/plex/__init__.py | 205 +++----- homeassistant/components/plex/config_flow.py | 171 +++++++ homeassistant/components/plex/const.py | 1 + homeassistant/components/plex/errors.py | 14 + homeassistant/components/plex/manifest.json | 3 +- homeassistant/components/plex/media_player.py | 30 +- homeassistant/components/plex/sensor.py | 23 +- homeassistant/components/plex/server.py | 16 +- homeassistant/components/plex/strings.json | 33 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/plex/__init__.py | 1 + tests/components/plex/mock_classes.py | 35 ++ tests/components/plex/test_config_flow.py | 454 ++++++++++++++++++ 17 files changed, 831 insertions(+), 167 deletions(-) create mode 100644 homeassistant/components/plex/config_flow.py create mode 100644 homeassistant/components/plex/errors.py create mode 100644 homeassistant/components/plex/strings.json create mode 100644 tests/components/plex/__init__.py create mode 100644 tests/components/plex/mock_classes.py create mode 100644 tests/components/plex/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 824fb3828f2d32..0e4199bb09770e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -479,7 +479,10 @@ omit = homeassistant/components/pioneer/media_player.py homeassistant/components/pjlink/media_player.py homeassistant/components/plaato/* - homeassistant/components/plex/* + homeassistant/components/plex/__init__.py + homeassistant/components/plex/media_player.py + homeassistant/components/plex/sensor.py + homeassistant/components/plex/server.py homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 827e05a424be85..15fcfc15338da2 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -50,6 +50,7 @@ SERVICE_DAIKIN: "daikin", SERVICE_TELLDUSLIVE: "tellduslive", SERVICE_IGD: "upnp", + SERVICE_PLEX: "plex", } SERVICE_HANDLERS = { @@ -69,7 +70,6 @@ SERVICE_FREEBOX: ("freebox", None), SERVICE_YEELIGHT: ("yeelight", None), "panasonic_viera": ("media_player", "panasonic_viera"), - SERVICE_PLEX: ("plex", None), "yamaha": ("media_player", "yamaha"), "logitech_mediaserver": ("media_player", "squeezebox"), "directv": ("media_player", "directv"), diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 69e77c8854fef1..665091d69b9b1b 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -5,7 +5,7 @@ import requests.exceptions import voluptuous as vol -from homeassistant.components.discovery import SERVICE_PLEX +from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( CONF_HOST, @@ -16,20 +16,18 @@ CONF_VERIFY_SSL, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.util.json import load_json, save_json from .const import ( - CONF_SERVER, CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, + CONF_SERVER, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN as PLEX_DOMAIN, PLATFORMS, - PLEX_CONFIG_FILE, PLEX_MEDIA_PLAYER_OPTIONS, + PLEX_SERVER_CONFIG, SERVERS, ) from .server import PlexServer @@ -58,151 +56,76 @@ CONFIG_SCHEMA = vol.Schema({PLEX_DOMAIN: SERVER_CONFIG_SCHEMA}, extra=vol.ALLOW_EXTRA) -CONFIGURING = "configuring" _LOGGER = logging.getLogger(__package__) def setup(hass, config): """Set up the Plex component.""" + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}}) - def server_discovered(service, info): - """Pass back discovered Plex server details.""" - if hass.data[PLEX_DOMAIN][SERVERS]: - _LOGGER.debug("Plex server already configured, ignoring discovery.") - return - _LOGGER.debug("Discovered Plex server: %s:%s", info["host"], info["port"]) - setup_plex(discovery_info=info) - - def setup_plex(config=None, discovery_info=None, configurator_info=None): - """Return assembled server_config dict.""" - json_file = hass.config.path(PLEX_CONFIG_FILE) - file_config = load_json(json_file) - host_and_port = None - - if config: - server_config = config - if CONF_HOST in server_config: - host_and_port = ( - f"{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}" - ) - if MP_DOMAIN in server_config: - hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = server_config.pop(MP_DOMAIN) - elif file_config: - _LOGGER.debug("Loading config from %s", json_file) - host_and_port, server_config = file_config.popitem() - server_config[CONF_VERIFY_SSL] = server_config.pop("verify") - elif discovery_info: - server_config = {} - host_and_port = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" - elif configurator_info: - server_config = configurator_info - host_and_port = server_config["host_and_port"] - else: - discovery.listen(hass, SERVICE_PLEX, server_discovered) - return True - - if host_and_port: - use_ssl = server_config.get(CONF_SSL, DEFAULT_SSL) - http_prefix = "https" if use_ssl else "http" - server_config[CONF_URL] = f"{http_prefix}://{host_and_port}" - - plex_server = PlexServer(server_config) - try: - plex_server.connect() - except requests.exceptions.ConnectionError as error: - _LOGGER.error( - "Plex server could not be reached, please verify host and port: [%s]", - error, - ) - return False - except ( - plexapi.exceptions.BadRequest, - plexapi.exceptions.Unauthorized, - plexapi.exceptions.NotFound, - ) as error: - _LOGGER.error( - "Connection to Plex server failed, please verify token and SSL settings: [%s]", - error, - ) - request_configuration(host_and_port) - return False - else: - hass.data[PLEX_DOMAIN][SERVERS][ - plex_server.machine_identifier - ] = plex_server - - if host_and_port in hass.data[PLEX_DOMAIN][CONFIGURING]: - request_id = hass.data[PLEX_DOMAIN][CONFIGURING].pop(host_and_port) - configurator = hass.components.configurator - configurator.request_done(request_id) - _LOGGER.debug("Discovery configuration done") - if configurator_info: - # Write plex.conf if created via discovery/configurator - save_json( - hass.config.path(PLEX_CONFIG_FILE), - { - host_and_port: { - CONF_TOKEN: server_config[CONF_TOKEN], - CONF_SSL: use_ssl, - "verify": server_config[CONF_VERIFY_SSL], - } - }, - ) - - if not hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS): - hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = MEDIA_PLAYER_SCHEMA({}) - - for platform in PLATFORMS: - hass.helpers.discovery.load_platform( - platform, PLEX_DOMAIN, {}, original_config - ) - - return True - - def request_configuration(host_and_port): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - if host_and_port in hass.data[PLEX_DOMAIN][CONFIGURING]: - configurator.notify_errors( - hass.data[PLEX_DOMAIN][CONFIGURING][host_and_port], - "Failed to register, please try again.", - ) - return - - def plex_configuration_callback(data): - """Handle configuration changes.""" - config = { - "host_and_port": host_and_port, - CONF_TOKEN: data.get("token"), - CONF_SSL: cv.boolean(data.get("ssl")), - CONF_VERIFY_SSL: cv.boolean(data.get("verify_ssl")), - } - setup_plex(configurator_info=config) - - hass.data[PLEX_DOMAIN][CONFIGURING][ - host_and_port - ] = configurator.request_config( - "Plex Media Server", - plex_configuration_callback, - description="Enter the X-Plex-Token", - entity_picture="/static/images/logo_plex_mediaserver.png", - submit_caption="Confirm", - fields=[ - {"id": "token", "name": "X-Plex-Token", "type": ""}, - {"id": "ssl", "name": "Use SSL", "type": ""}, - {"id": "verify_ssl", "name": "Verify SSL", "type": ""}, - ], + plex_config = config.get(PLEX_DOMAIN, {}) + if plex_config: + _setup_plex(hass, plex_config) + + return True + + +def _setup_plex(hass, config): + """Pass configuration to a config flow.""" + server_config = dict(config) + if MP_DOMAIN in server_config: + hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = server_config.pop(MP_DOMAIN) + if CONF_HOST in server_config: + prefix = "https" if server_config.pop(CONF_SSL) else "http" + server_config[ + CONF_URL + ] = f"{prefix}://{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}" + hass.async_create_task( + hass.config_entries.flow.async_init( + PLEX_DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=server_config, ) + ) - # End of inner functions. - - original_config = config - hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, CONFIGURING: {}}) +async def async_setup_entry(hass, entry): + """Set up Plex from a config entry.""" + server_config = entry.data[PLEX_SERVER_CONFIG] - if hass.data[PLEX_DOMAIN][SERVERS]: - _LOGGER.debug("Plex server already configured") + plex_server = PlexServer(server_config) + try: + await hass.async_add_executor_job(plex_server.connect) + except requests.exceptions.ConnectionError as error: + _LOGGER.error( + "Plex server (%s) could not be reached: [%s]", + server_config[CONF_URL], + error, + ) + return False + except ( + plexapi.exceptions.BadRequest, + plexapi.exceptions.Unauthorized, + plexapi.exceptions.NotFound, + ) as error: + _LOGGER.error( + "Login to %s failed, verify token and SSL settings: [%s]", + server_config[CONF_SERVER], + error, + ) return False - plex_config = config.get(PLEX_DOMAIN, {}) - return setup_plex(config=plex_config) + _LOGGER.debug( + "Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use + ) + hass.data[PLEX_DOMAIN][SERVERS][plex_server.machine_identifier] = plex_server + + if not hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS): + hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = MEDIA_PLAYER_SCHEMA({}) + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py new file mode 100644 index 00000000000000..3c683c802f5ccf --- /dev/null +++ b/homeassistant/components/plex/config_flow.py @@ -0,0 +1,171 @@ +"""Config flow for Plex.""" +import logging + +import plexapi.exceptions +import requests.exceptions +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_URL, CONF_TOKEN, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.core import callback +from homeassistant.util.json import load_json + +from .const import ( # pylint: disable=unused-import + CONF_SERVER, + CONF_SERVER_IDENTIFIER, + DEFAULT_VERIFY_SSL, + DOMAIN, + PLEX_CONFIG_FILE, + PLEX_SERVER_CONFIG, +) +from .errors import NoServersFound, ServerNotSpecified +from .server import PlexServer + +USER_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): str}) + +_LOGGER = logging.getLogger(__package__) + + +@callback +def configured_servers(hass): + """Return a set of the configured Plex servers.""" + return set( + entry.data[CONF_SERVER_IDENTIFIER] + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Plex config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the Plex flow.""" + self.current_login = {} + self.available_servers = None + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is not None: + return await self.async_step_server_validate(user_input) + + return self.async_show_form(step_id="user", data_schema=USER_SCHEMA, errors={}) + + async def async_step_server_validate(self, server_config): + """Validate a provided configuration.""" + errors = {} + self.current_login = server_config + + plex_server = PlexServer(server_config) + try: + await self.hass.async_add_executor_job(plex_server.connect) + + except NoServersFound: + errors["base"] = "no_servers" + except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): + _LOGGER.error("Invalid credentials provided, config not created") + errors["base"] = "faulty_credentials" + except (plexapi.exceptions.NotFound, requests.exceptions.ConnectionError): + _LOGGER.error( + "Plex server could not be reached: %s", server_config[CONF_URL] + ) + errors["base"] = "not_found" + + except ServerNotSpecified as available_servers: + self.available_servers = available_servers.args[0] + return await self.async_step_select_server() + + except Exception as error: # pylint: disable=broad-except + _LOGGER.error("Unknown error connecting to Plex server: %s", error) + return self.async_abort(reason="unknown") + + if errors: + return self.async_show_form( + step_id="user", data_schema=USER_SCHEMA, errors=errors + ) + + server_id = plex_server.machine_identifier + + for entry in self._async_current_entries(): + if entry.data[CONF_SERVER_IDENTIFIER] == server_id: + return self.async_abort(reason="already_configured") + + url = plex_server.url_in_use + token = server_config.get(CONF_TOKEN) + + entry_config = {CONF_URL: url} + if token: + entry_config[CONF_TOKEN] = token + if url.startswith("https"): + entry_config[CONF_VERIFY_SSL] = server_config.get( + CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL + ) + + _LOGGER.debug("Valid config created for %s", plex_server.friendly_name) + + return self.async_create_entry( + title=plex_server.friendly_name, + data={ + CONF_SERVER: plex_server.friendly_name, + CONF_SERVER_IDENTIFIER: server_id, + PLEX_SERVER_CONFIG: entry_config, + }, + ) + + async def async_step_select_server(self, user_input=None): + """Use selected Plex server.""" + config = dict(self.current_login) + if user_input is not None: + config[CONF_SERVER] = user_input[CONF_SERVER] + return await self.async_step_server_validate(config) + + configured = configured_servers(self.hass) + available_servers = [ + name + for (name, server_id) in self.available_servers + if server_id not in configured + ] + + if not available_servers: + return self.async_abort(reason="all_configured") + if len(available_servers) == 1: + config[CONF_SERVER] = available_servers[0] + return await self.async_step_server_validate(config) + + return self.async_show_form( + step_id="select_server", + data_schema=vol.Schema( + {vol.Required(CONF_SERVER): vol.In(available_servers)} + ), + errors={}, + ) + + async def async_step_discovery(self, discovery_info): + """Set default host and port from discovery.""" + if self._async_current_entries() or self._async_in_progress(): + # Skip discovery if a config already exists or is in progress. + return self.async_abort(reason="already_configured") + + json_file = self.hass.config.path(PLEX_CONFIG_FILE) + file_config = await self.hass.async_add_executor_job(load_json, json_file) + + if file_config: + host_and_port, host_config = file_config.popitem() + prefix = "https" if host_config[CONF_SSL] else "http" + + server_config = { + CONF_URL: f"{prefix}://{host_and_port}", + CONF_TOKEN: host_config[CONF_TOKEN], + CONF_VERIFY_SSL: host_config["verify"], + } + _LOGGER.info("Imported legacy config, file can be removed: %s", json_file) + return await self.async_step_server_validate(server_config) + + return await self.async_step_user() + + async def async_step_import(self, import_config): + """Import from Plex configuration.""" + _LOGGER.debug("Imported Plex configuration") + return await self.async_step_server_validate(import_config) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 6f19623c809e86..e77ac303bf1a25 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -14,5 +14,6 @@ PLEX_SERVER_CONFIG = "server_config" CONF_SERVER = "server" +CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" CONF_SHOW_ALL_CONTROLS = "show_all_controls" diff --git a/homeassistant/components/plex/errors.py b/homeassistant/components/plex/errors.py new file mode 100644 index 00000000000000..11c15404f4505c --- /dev/null +++ b/homeassistant/components/plex/errors.py @@ -0,0 +1,14 @@ +"""Errors for the Plex component.""" +from homeassistant.exceptions import HomeAssistantError + + +class PlexException(HomeAssistantError): + """Base class for Plex exceptions.""" + + +class NoServersFound(PlexException): + """No servers found on Plex account.""" + + +class ServerNotSpecified(PlexException): + """Multiple servers linked to account without choice provided.""" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 4269400dc2456e..94d990952a684e 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -1,11 +1,12 @@ { "domain": "plex", "name": "Plex", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/plex", "requirements": [ "plexapi==3.0.6" ], - "dependencies": ["configurator"], + "dependencies": [], "codeowners": [ "@jjlawren" ] diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index cfc63948bee80c..bc19ff41dfedff 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -35,26 +35,40 @@ from .const import ( CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, + CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, PLEX_MEDIA_PLAYER_OPTIONS, SERVERS, ) -SERVER_SETUP = "server_setup" - -_CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities_callback, discovery_info=None): - """Set up the Plex platform.""" - if discovery_info is None: - return +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Plex media_player platform. + + Deprecated. + """ + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Plex media_player from a config entry.""" + + def add_entities(entities, update_before_add=False): + """Sync version of async add entities.""" + hass.add_job(async_add_entities, entities, update_before_add) + + hass.async_add_executor_job(_setup_platform, hass, config_entry, add_entities) + - plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] +def _setup_platform(hass, config_entry, add_entities_callback): + """Set up the Plex media_player platform.""" + server_id = config_entry.data[CONF_SERVER_IDENTIFIER] config = hass.data[PLEX_MEDIA_PLAYER_OPTIONS] + plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] plex_clients = {} plex_sessions = {} track_time_interval(hass, lambda now: update_devices(), timedelta(seconds=10)) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index f469e95da808e0..7d5b54356a0c82 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -8,21 +8,26 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from .const import DOMAIN as PLEX_DOMAIN, SERVERS +from .const import CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, SERVERS -DEFAULT_NAME = "Plex" _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Plex sensor.""" - if discovery_info is None: - return +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Plex sensor platform. - plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] - add_entities([PlexSensor(plexserver)], True) + Deprecated. + """ + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Plex sensor from a config entry.""" + server_id = config_entry.data[CONF_SERVER_IDENTIFIER] + sensor = PlexSensor(hass.data[PLEX_DOMAIN][SERVERS][server_id]) + async_add_entities([sensor], True) class PlexSensor(Entity): @@ -30,10 +35,10 @@ class PlexSensor(Entity): def __init__(self, plex_server): """Initialize the sensor.""" - self._name = DEFAULT_NAME self._state = None self._now_playing = [] self._server = plex_server + self._name = f"Plex ({plex_server.friendly_name})" self._unique_id = f"sensor-{plex_server.machine_identifier}" @property diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 962e074996f5e5..f41a9bdabae183 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,6 +1,4 @@ """Shared class to maintain Plex server instances.""" -import logging - import plexapi.myplex import plexapi.server from requests import Session @@ -8,8 +6,7 @@ from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from .const import CONF_SERVER, DEFAULT_VERIFY_SSL - -_LOGGER = logging.getLogger(__package__) +from .errors import NoServersFound, ServerNotSpecified class PlexServer: @@ -29,8 +26,16 @@ def connect(self): def _set_missing_url(): account = plexapi.myplex.MyPlexAccount(token=self._token) available_servers = [ - x.name for x in account.resources() if "server" in x.provides + (x.name, x.clientIdentifier) + for x in account.resources() + if "server" in x.provides ] + + if not available_servers: + raise NoServersFound + if not self._server_name and len(available_servers) > 1: + raise ServerNotSpecified(available_servers) + server_choice = ( self._server_name if self._server_name else available_servers[0] ) @@ -47,7 +52,6 @@ def _connect_with_url(): self._plex_server = plexapi.server.PlexServer( self._url, self._token, session ) - _LOGGER.debug("Connected to: %s (%s)", self.friendly_name, self.url_in_use) if self._token and not self._url: _set_missing_url() diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json new file mode 100644 index 00000000000000..396a3387fee295 --- /dev/null +++ b/homeassistant/components/plex/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "title": "Plex", + "step": { + "select_server": { + "title": "Select Plex server", + "description": "Multiple servers available, select one:", + "data": { + "server": "Server" + } + }, + "user": { + "title": "Connect Plex server", + "description": "Enter a Plex token for automatic setup.", + "data": { + "token": "Plex token" + } + } + }, + "error": { + "faulty_credentials": "Authorization failed", + "no_servers": "No servers linked to account", + "not_found": "Plex server not found" + }, + "abort": { + "all_configured": "All linked servers already configured", + "already_configured": "This Plex server is already configured", + "already_in_progress": "Plex is being configured", + "invalid_import": "Imported configuration is invalid", + "unknown": "Failed for unknown reason" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7f3f5c1f20d23b..9ddae5acdb9941 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -45,6 +45,7 @@ "openuv", "owntracks", "plaato", + "plex", "point", "ps4", "rainmachine", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83ec2d1d2c1023..ef8618f146bbb3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -249,6 +249,9 @@ pexpect==4.6.0 # homeassistant.components.pilight pilight==0.1.1 +# homeassistant.components.plex +plexapi==3.0.6 + # homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index ff2943a583b3f8..72fb9ff5a44233 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -110,6 +110,7 @@ "paho-mqtt", "pexpect", "pilight", + "plexapi", "pmsensor", "prometheus_client", "ptvsd", diff --git a/tests/components/plex/__init__.py b/tests/components/plex/__init__.py new file mode 100644 index 00000000000000..9c9c00d87ace68 --- /dev/null +++ b/tests/components/plex/__init__.py @@ -0,0 +1 @@ +"""Tests for the Plex component.""" diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py new file mode 100644 index 00000000000000..d027087828073c --- /dev/null +++ b/tests/components/plex/mock_classes.py @@ -0,0 +1,35 @@ +"""Mock classes used in tests.""" + +MOCK_HOST_1 = "1.2.3.4" +MOCK_PORT_1 = "32400" +MOCK_HOST_2 = "4.3.2.1" +MOCK_PORT_2 = "32400" + + +class MockAvailableServer: # pylint: disable=too-few-public-methods + """Mock avilable server objects.""" + + def __init__(self, name, client_id): + """Initialize the object.""" + self.name = name + self.clientIdentifier = client_id # pylint: disable=invalid-name + self.provides = ["server"] + + +class MockConnection: # pylint: disable=too-few-public-methods + """Mock a single account resource connection object.""" + + def __init__(self, ssl): + """Initialize the object.""" + prefix = "https" if ssl else "http" + self.httpuri = f"{prefix}://{MOCK_HOST_1}:{MOCK_PORT_1}" + self.uri = "{prefix}://{MOCK_HOST_2}:{MOCK_PORT_2}" + self.local = True + + +class MockConnections: # pylint: disable=too-few-public-methods + """Mock a list of resource connections.""" + + def __init__(self, ssl=False): + """Initialize the object.""" + self.connections = [MockConnection(ssl)] diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py new file mode 100644 index 00000000000000..9c9c1b625259f9 --- /dev/null +++ b/tests/components/plex/test_config_flow.py @@ -0,0 +1,454 @@ +"""Tests for Plex config flow.""" +from unittest.mock import MagicMock, Mock, patch, PropertyMock +import plexapi.exceptions +import requests.exceptions + +from homeassistant.components.plex import config_flow +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL + +from tests.common import MockConfigEntry + +from .mock_classes import MOCK_HOST_1, MOCK_PORT_1, MockAvailableServer, MockConnections + +MOCK_NAME_1 = "Plex Server 1" +MOCK_ID_1 = "unique_id_123" +MOCK_NAME_2 = "Plex Server 2" +MOCK_ID_2 = "unique_id_456" +MOCK_TOKEN = "secret_token" +MOCK_FILE_CONTENTS = { + f"{MOCK_HOST_1}:{MOCK_PORT_1}": {"ssl": False, "token": MOCK_TOKEN, "verify": True} +} +MOCK_SERVER_1 = MockAvailableServer(MOCK_NAME_1, MOCK_ID_1) +MOCK_SERVER_2 = MockAvailableServer(MOCK_NAME_2, MOCK_ID_2) + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.PlexFlowHandler() + flow.hass = hass + return flow + + +async def test_bad_credentials(hass): + """Test when provided credentials are rejected.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch( + "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized + ): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"]["base"] == "faulty_credentials" + + +async def test_import_file_from_discovery(hass): + """Test importing a legacy file during discovery.""" + + file_host_and_port, file_config = list(MOCK_FILE_CONTENTS.items())[0] + used_url = f"http://{file_host_and_port}" + + with patch("plexapi.server.PlexServer") as mock_plex_server, patch( + "homeassistant.components.plex.config_flow.load_json", + return_value=MOCK_FILE_CONTENTS, + ): + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_ID_1 + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_NAME_1 + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=used_url) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "discovery"}, + data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == MOCK_NAME_1 + assert result["data"][config_flow.CONF_SERVER] == MOCK_NAME_1 + assert result["data"][config_flow.CONF_SERVER_IDENTIFIER] == MOCK_ID_1 + assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] == used_url + assert ( + result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] + == file_config[CONF_TOKEN] + ) + + +async def test_discovery(hass): + """Test starting a flow from discovery.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "discovery"}, + data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + +async def test_discovery_while_in_progress(hass): + """Test starting a flow from discovery.""" + + await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "discovery"}, + data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_import_success(hass): + """Test a successful configuration import.""" + + mock_connections = MockConnections(ssl=True) + + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) + + with patch("plexapi.server.PlexServer") as mock_plex_server: + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_SERVER_1.clientIdentifier + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_SERVER_1.name + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "import"}, + data={ + CONF_TOKEN: MOCK_TOKEN, + CONF_URL: f"https://{MOCK_HOST_1}:{MOCK_PORT_1}", + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == MOCK_SERVER_1.name + assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == MOCK_SERVER_1.clientIdentifier + ) + assert ( + result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] + == mock_connections.connections[0].httpuri + ) + assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + + +async def test_import_bad_hostname(hass): + """Test when an invalid address is provided.""" + + with patch( + "plexapi.server.PlexServer", side_effect=requests.exceptions.ConnectionError + ): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "import"}, + data={ + CONF_TOKEN: MOCK_TOKEN, + CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}", + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"]["base"] == "not_found" + + +async def test_unknown_exception(hass): + """Test when an unknown exception is encountered.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "user"}, + data={CONF_TOKEN: MOCK_TOKEN}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "unknown" + + +async def test_no_servers_found(hass): + """Test when no servers are on an account.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[]) + + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"]["base"] == "no_servers" + + +async def test_single_available_server(hass): + """Test creating an entry with one server available.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + mock_connections = MockConnections() + + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) + + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( + "plexapi.server.PlexServer" + ) as mock_plex_server: + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_SERVER_1.clientIdentifier + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_SERVER_1.name + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "create_entry" + assert result["title"] == MOCK_SERVER_1.name + assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == MOCK_SERVER_1.clientIdentifier + ) + assert ( + result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] + == mock_connections.connections[0].httpuri + ) + assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + + +async def test_multiple_servers_with_selection(hass): + """Test creating an entry with multiple servers available.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) + mm_plex_account.resource = Mock(return_value=mock_connections) + + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( + "plexapi.server.PlexServer" + ) as mock_plex_server: + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_SERVER_1.clientIdentifier + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_SERVER_1.name + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "form" + assert result["step_id"] == "select_server" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={config_flow.CONF_SERVER: MOCK_SERVER_1.name} + ) + + assert result["type"] == "create_entry" + assert result["title"] == MOCK_SERVER_1.name + assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == MOCK_SERVER_1.clientIdentifier + ) + assert ( + result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] + == mock_connections.connections[0].httpuri + ) + assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + + +async def test_adding_last_unconfigured_server(hass): + """Test automatically adding last unconfigured server when multiple servers on account.""" + + MockConfigEntry( + domain=config_flow.DOMAIN, + data={ + config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_2, + config_flow.CONF_SERVER: MOCK_NAME_2, + }, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) + mm_plex_account.resource = Mock(return_value=mock_connections) + + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( + "plexapi.server.PlexServer" + ) as mock_plex_server: + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_SERVER_1.clientIdentifier + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_SERVER_1.name + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "create_entry" + assert result["title"] == MOCK_SERVER_1.name + assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == MOCK_SERVER_1.clientIdentifier + ) + assert ( + result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] + == mock_connections.connections[0].httpuri + ) + assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + + +async def test_already_configured(hass): + """Test a duplicated successful flow.""" + + flow = init_config_flow(hass) + MockConfigEntry( + domain=config_flow.DOMAIN, data={config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_1} + ).add_to_hass(hass) + + mock_connections = MockConnections() + + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) + + with patch("plexapi.server.PlexServer") as mock_plex_server: + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_SERVER_1.clientIdentifier + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_SERVER_1.name + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + result = await flow.async_step_import( + {CONF_TOKEN: MOCK_TOKEN, CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}"} + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_all_available_servers_configured(hass): + """Test when all available servers are already configured.""" + + MockConfigEntry( + domain=config_flow.DOMAIN, + data={ + config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_1, + config_flow.CONF_SERVER: MOCK_NAME_1, + }, + ).add_to_hass(hass) + + MockConfigEntry( + domain=config_flow.DOMAIN, + data={ + config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_2, + config_flow.CONF_SERVER: MOCK_NAME_2, + }, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) + mm_plex_account.resource = Mock(return_value=mock_connections) + + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "abort" + assert result["reason"] == "all_configured" From c8fb7ce98b0cabaccfd62135652998c5bb434622 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Sep 2019 23:30:25 +0200 Subject: [PATCH 074/296] Bump restrictedpython to 5.0 (#26741) --- homeassistant/components/python_script/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 610ec92a2b3939..83d70830b11a43 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -3,8 +3,8 @@ "name": "Python script", "documentation": "https://www.home-assistant.io/components/python_script", "requirements": [ - "restrictedpython==4.0" + "restrictedpython==5.0" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 8cbf9230ec0fc6..99d81158edbae5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1666,7 +1666,7 @@ recollect-waste==1.0.1 regenmaschine==1.5.1 # homeassistant.components.python_script -restrictedpython==4.0 +restrictedpython==5.0 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef8618f146bbb3..3d0d42cc8472bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -371,7 +371,7 @@ pywebpush==1.9.2 regenmaschine==1.5.1 # homeassistant.components.python_script -restrictedpython==4.0 +restrictedpython==5.0 # homeassistant.components.rflink rflink==0.0.46 From b68b8430a4220a2b408fdbae55d9e7f8e97fcd84 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Fri, 20 Sep 2019 05:31:54 +0800 Subject: [PATCH 075/296] Izone component (#24550) * iZone component * Rename constants to const. * Changes as per code review. * Stop listening if discovery times out. * Unload properly * Changes as per code review * Climate 1.0 * Use dispatcher instead of listener * Free air settings * Test case for config flow. * Changes as per code review * Fix error on shutdown * Changes as per code review * Lint fix * Black formatting * Black on test * Fix test * Lint fix * Formatting * Updated requirements * Remaining patches * Per code r/v --- .coveragerc | 3 + CODEOWNERS | 1 + .../components/izone/.translations/en.json | 15 + homeassistant/components/izone/__init__.py | 67 +++ homeassistant/components/izone/climate.py | 546 ++++++++++++++++++ homeassistant/components/izone/config_flow.py | 45 ++ homeassistant/components/izone/const.py | 14 + homeassistant/components/izone/discovery.py | 87 +++ homeassistant/components/izone/manifest.json | 9 + homeassistant/components/izone/strings.json | 15 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/izone/__init__.py | 1 + tests/components/izone/test_config_flow.py | 83 +++ 16 files changed, 894 insertions(+) create mode 100644 homeassistant/components/izone/.translations/en.json create mode 100644 homeassistant/components/izone/__init__.py create mode 100644 homeassistant/components/izone/climate.py create mode 100644 homeassistant/components/izone/config_flow.py create mode 100644 homeassistant/components/izone/const.py create mode 100644 homeassistant/components/izone/discovery.py create mode 100644 homeassistant/components/izone/manifest.json create mode 100644 homeassistant/components/izone/strings.json create mode 100644 tests/components/izone/__init__.py create mode 100644 tests/components/izone/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 0e4199bb09770e..5d5b0f6c81b09b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -295,6 +295,9 @@ omit = homeassistant/components/iaqualink/sensor.py homeassistant/components/iaqualink/switch.py homeassistant/components/icloud/device_tracker.py + homeassistant/components/izone/climate.py + homeassistant/components/izone/discovery.py + homeassistant/components/izone/__init__.py homeassistant/components/idteck_prox/* homeassistant/components/ifttt/* homeassistant/components/iglo/light.py diff --git a/CODEOWNERS b/CODEOWNERS index c454514912ca51..19dd0d5c8b69f3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -144,6 +144,7 @@ homeassistant/components/ios/* @robbiet480 homeassistant/components/ipma/* @dgomes homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 +homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/keba/* @dannerph homeassistant/components/knx/* @Julius2342 diff --git a/homeassistant/components/izone/.translations/en.json b/homeassistant/components/izone/.translations/en.json new file mode 100644 index 00000000000000..5293ad2a1fec34 --- /dev/null +++ b/homeassistant/components/izone/.translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No iZone devices found on the network.", + "single_instance_allowed": "Only a single configuration of iZone is necessary." + }, + "step": { + "confirm": { + "description": "Do you want to set up iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py new file mode 100644 index 00000000000000..7f80fb077cf92c --- /dev/null +++ b/homeassistant/components/izone/__init__.py @@ -0,0 +1,67 @@ +""" +Platform for the iZone AC. + +For more details about this component, please refer to the documentation +https://home-assistant.io/components/izone/ +""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EXCLUDE +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import IZONE, DATA_CONFIG +from .discovery import async_start_discovery_service, async_stop_discovery_service + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + IZONE: vol.Schema( + { + vol.Optional(CONF_EXCLUDE, default=[]): vol.All( + cv.ensure_list, [cv.string] + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Register the iZone component config.""" + conf = config.get(IZONE) + if not conf: + return True + + hass.data[DATA_CONFIG] = conf + + # Explicitly added in the config file, create a config entry. + hass.async_create_task( + hass.config_entries.flow.async_init( + IZONE, context={"source": config_entries.SOURCE_IMPORT} + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up from a config entry.""" + await async_start_discovery_service(hass) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "climate") + ) + return True + + +async def async_unload_entry(hass, entry): + """Unload the config entry and stop discovery process.""" + await async_stop_discovery_service(hass) + await hass.config_entries.async_forward_entry_unload(entry, "climate") + return True diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py new file mode 100644 index 00000000000000..c932c66627bcae --- /dev/null +++ b/homeassistant/components/izone/climate.py @@ -0,0 +1,546 @@ +"""Support for the iZone HVAC.""" +import logging +from typing import Optional, List + +from pizone import Zone, Controller + +from homeassistant.core import callback +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + FAN_AUTO, + PRESET_ECO, + PRESET_NONE, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_HALVES, + TEMP_CELSIUS, + CONF_EXCLUDE, +) +from homeassistant.helpers.temperature import display_temp as show_temp +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import ( + DATA_DISCOVERY_SERVICE, + IZONE, + DISPATCH_CONTROLLER_DISCOVERED, + DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_RECONNECTED, + DISPATCH_CONTROLLER_UPDATE, + DISPATCH_ZONE_UPDATE, + DATA_CONFIG, +) + +_LOGGER = logging.getLogger(__name__) + +_IZONE_FAN_TO_HA = { + Controller.Fan.LOW: FAN_LOW, + Controller.Fan.MED: FAN_MEDIUM, + Controller.Fan.HIGH: FAN_HIGH, + Controller.Fan.AUTO: FAN_AUTO, +} + + +async def async_setup_entry( + hass: HomeAssistantType, config: ConfigType, async_add_entities +): + """Initialize an IZone Controller.""" + disco = hass.data[DATA_DISCOVERY_SERVICE] + + @callback + def init_controller(ctrl: Controller): + """Register the controller device and the containing zones.""" + conf = hass.data.get(DATA_CONFIG) # type: ConfigType + + # Filter out any entities excluded in the config file + if conf and ctrl.device_uid in conf[CONF_EXCLUDE]: + _LOGGER.info("Controller UID=%s ignored as excluded", ctrl.device_uid) + return + _LOGGER.info("Controller UID=%s discovered", ctrl.device_uid) + + device = ControllerDevice(ctrl) + async_add_entities([device]) + async_add_entities(device.zones.values()) + + # create any components not yet created + for controller in disco.pi_disco.controllers.values(): + init_controller(controller) + + # connect to register any further components + async_dispatcher_connect(hass, DISPATCH_CONTROLLER_DISCOVERED, init_controller) + + return True + + +class ControllerDevice(ClimateDevice): + """Representation of iZone Controller.""" + + def __init__(self, controller: Controller) -> None: + """Initialise ControllerDevice.""" + self._controller = controller + + self._supported_features = SUPPORT_FAN_MODE + + if ( + controller.ras_mode == "master" and controller.zone_ctrl == 13 + ) or controller.ras_mode == "RAS": + self._supported_features |= SUPPORT_TARGET_TEMPERATURE + + self._state_to_pizone = { + HVAC_MODE_COOL: Controller.Mode.COOL, + HVAC_MODE_HEAT: Controller.Mode.HEAT, + HVAC_MODE_HEAT_COOL: Controller.Mode.AUTO, + HVAC_MODE_FAN_ONLY: Controller.Mode.VENT, + HVAC_MODE_DRY: Controller.Mode.DRY, + } + if controller.free_air_enabled: + self._supported_features |= SUPPORT_PRESET_MODE + + self._fan_to_pizone = {} + for fan in controller.fan_modes: + self._fan_to_pizone[_IZONE_FAN_TO_HA[fan]] = fan + self._available = True + + self._device_info = { + "identifiers": {(IZONE, self.unique_id)}, + "name": self.name, + "manufacturer": "IZone", + "model": self._controller.sys_type, + } + + # Create the zones + self.zones = {} + for zone in controller.zones: + self.zones[zone] = ZoneDevice(self, zone) + + async def async_added_to_hass(self): + """Call on adding to hass.""" + # Register for connect/disconnect/update events + @callback + def controller_disconnected(ctrl: Controller, ex: Exception) -> None: + """Disconnected from controller.""" + if ctrl is not self._controller: + return + self.set_available(False, ex) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_DISCONNECTED, controller_disconnected + ) + ) + + @callback + def controller_reconnected(ctrl: Controller) -> None: + """Reconnected to controller.""" + if ctrl is not self._controller: + return + self.set_available(True) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_RECONNECTED, controller_reconnected + ) + ) + + @callback + def controller_update(ctrl: Controller) -> None: + """Handle controller data updates.""" + if ctrl is not self._controller: + return + self.async_schedule_update_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_UPDATE, controller_update + ) + ) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @callback + def set_available(self, available: bool, ex: Exception = None) -> None: + """ + Set availability for the controller. + + Also sets zone availability as they follow the same availability. + """ + if self.available == available: + return + + if available: + _LOGGER.info("Reconnected controller %s ", self._controller.device_uid) + else: + _LOGGER.info( + "Controller %s disconnected due to exception: %s", + self._controller.device_uid, + ex, + ) + + self._available = available + self.async_schedule_update_ha_state() + for zone in self.zones.values(): + zone.async_schedule_update_ha_state() + + @property + def device_info(self): + """Return the device info for the iZone system.""" + return self._device_info + + @property + def unique_id(self): + """Return the ID of the controller device.""" + return self._controller.device_uid + + @property + def name(self) -> str: + """Return the name of the entity.""" + return f"iZone Controller {self._controller.device_uid}" + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return self._supported_features + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def precision(self) -> float: + """Return the precision of the system.""" + return PRECISION_HALVES + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + return { + "supply_temperature": show_temp( + self.hass, + self.supply_temperature, + self.temperature_unit, + self.precision, + ), + "temp_setpoint": show_temp( + self.hass, + self._controller.temp_setpoint, + self.temperature_unit, + self.precision, + ), + } + + @property + def hvac_mode(self) -> str: + """Return current operation ie. heat, cool, idle.""" + if not self._controller.is_on: + return HVAC_MODE_OFF + mode = self._controller.mode + for (key, value) in self._state_to_pizone.items(): + if value == mode: + return key + assert False, "Should be unreachable" + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available operation modes.""" + if self._controller.free_air: + return [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY] + return [HVAC_MODE_OFF, *self._state_to_pizone] + + @property + def preset_mode(self): + """Eco mode is external air.""" + return PRESET_ECO if self._controller.free_air else PRESET_NONE + + @property + def preset_modes(self): + """Available preset modes, normal or eco.""" + if self._controller.free_air_enabled: + return [PRESET_NONE, PRESET_ECO] + return [PRESET_NONE] + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + if self._controller.mode == Controller.Mode.FREE_AIR: + return self._controller.temp_supply + return self._controller.temp_return + + @property + def target_temperature(self) -> Optional[float]: + """Return the temperature we try to reach.""" + if not self._supported_features & SUPPORT_TARGET_TEMPERATURE: + return None + return self._controller.temp_setpoint + + @property + def supply_temperature(self) -> float: + """Return the current supply, or in duct, temperature.""" + return self._controller.temp_supply + + @property + def target_temperature_step(self) -> Optional[float]: + """Return the supported step of target temperature.""" + return 0.5 + + @property + def fan_mode(self) -> Optional[str]: + """Return the fan setting.""" + return _IZONE_FAN_TO_HA[self._controller.fan] + + @property + def fan_modes(self) -> Optional[List[str]]: + """Return the list of available fan modes.""" + return list(self._fan_to_pizone) + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return self._controller.temp_min + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return self._controller.temp_max + + async def wrap_and_catch(self, coro): + """Catch any connection errors and set unavailable.""" + try: + await coro + except ConnectionError as ex: + self.set_available(False, ex) + else: + self.set_available(True) + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + if not self.supported_features & SUPPORT_TARGET_TEMPERATURE: + self.async_schedule_update_ha_state(True) + return + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + await self.wrap_and_catch(self._controller.set_temp_setpoint(temp)) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + fan = self._fan_to_pizone[fan_mode] + await self.wrap_and_catch(self._controller.set_fan(fan)) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target operation mode.""" + if hvac_mode == HVAC_MODE_OFF: + await self.wrap_and_catch(self._controller.set_on(False)) + return + if not self._controller.is_on: + await self.wrap_and_catch(self._controller.set_on(True)) + if self._controller.free_air: + return + mode = self._state_to_pizone[hvac_mode] + await self.wrap_and_catch(self._controller.set_mode(mode)) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode.""" + await self.wrap_and_catch( + self._controller.set_free_air(preset_mode == PRESET_ECO) + ) + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + await self.wrap_and_catch(self._controller.set_on(True)) + + +class ZoneDevice(ClimateDevice): + """Representation of iZone Zone.""" + + def __init__(self, controller: ControllerDevice, zone: Zone) -> None: + """Initialise ZoneDevice.""" + self._controller = controller + self._zone = zone + self._name = zone.name.title() + + self._supported_features = 0 + if zone.type != Zone.Type.AUTO: + self._state_to_pizone = { + HVAC_MODE_OFF: Zone.Mode.CLOSE, + HVAC_MODE_FAN_ONLY: Zone.Mode.OPEN, + } + else: + self._state_to_pizone = { + HVAC_MODE_OFF: Zone.Mode.CLOSE, + HVAC_MODE_FAN_ONLY: Zone.Mode.OPEN, + HVAC_MODE_HEAT_COOL: Zone.Mode.AUTO, + } + self._supported_features |= SUPPORT_TARGET_TEMPERATURE + + self._device_info = { + "identifiers": {(IZONE, controller.unique_id, zone.index)}, + "name": self.name, + "manufacturer": "IZone", + "via_device": (IZONE, controller.unique_id), + "model": zone.type.name.title(), + } + + async def async_added_to_hass(self): + """Call on adding to hass.""" + + @callback + def zone_update(ctrl: Controller, zone: Zone) -> None: + """Handle zone data updates.""" + if zone is not self._zone: + return + self._name = zone.name.title() + self.async_schedule_update_ha_state() + + self.async_on_remove( + async_dispatcher_connect(self.hass, DISPATCH_ZONE_UPDATE, zone_update) + ) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._controller.available + + @property + def assumed_state(self) -> bool: + """Return True if unable to access real state of the entity.""" + return self._controller.assumed_state + + @property + def device_info(self): + """Return the device info for the iZone system.""" + return self._device_info + + @property + def unique_id(self): + """Return the ID of the controller device.""" + return "{}_z{}".format(self._controller.unique_id, self._zone.index + 1) + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False + + @property + def supported_features(self): + """Return the list of supported features.""" + try: + if self._zone.mode == Zone.Mode.AUTO: + return self._supported_features + return self._supported_features & ~SUPPORT_TARGET_TEMPERATURE + except ConnectionError: + return None + + @property + def temperature_unit(self): + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_HALVES + + @property + def hvac_mode(self): + """Return current operation ie. heat, cool, idle.""" + mode = self._zone.mode + for (key, value) in self._state_to_pizone.items(): + if value == mode: + return key + return None + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + return list(self._state_to_pizone.keys()) + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._zone.temp_current + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self._zone.type != Zone.Type.AUTO: + return None + return self._zone.temp_setpoint + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 0.5 + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._controller.min_temp + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._controller.max_temp + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if self._zone.mode != Zone.Mode.AUTO: + return + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + await self._controller.wrap_and_catch(self._zone.set_temp_setpoint(temp)) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target operation mode.""" + mode = self._state_to_pizone[hvac_mode] + await self._controller.wrap_and_catch(self._zone.set_mode(mode)) + self.async_schedule_update_ha_state() + + @property + def is_on(self): + """Return true if on.""" + return self._zone.mode != Zone.Mode.CLOSE + + async def async_turn_on(self): + """Turn device on (open zone).""" + if self._zone.type == Zone.Type.AUTO: + await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.AUTO)) + else: + await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.OPEN)) + self.async_schedule_update_ha_state() + + async def async_turn_off(self): + """Turn device off (close zone).""" + await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.CLOSE)) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py new file mode 100644 index 00000000000000..eb57a36a2bb5f7 --- /dev/null +++ b/homeassistant/components/izone/config_flow.py @@ -0,0 +1,45 @@ +"""Config flow for izone.""" + +import logging +import asyncio + +from async_timeout import timeout + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import IZONE, TIMEOUT_DISCOVERY, DISPATCH_CONTROLLER_DISCOVERED + + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(hass): + from .discovery import async_start_discovery_service, async_stop_discovery_service + + controller_ready = asyncio.Event() + async_dispatcher_connect( + hass, DISPATCH_CONTROLLER_DISCOVERED, lambda x: controller_ready.set() + ) + + disco = await async_start_discovery_service(hass) + + try: + async with timeout(TIMEOUT_DISCOVERY): + await controller_ready.wait() + except asyncio.TimeoutError: + pass + + if not disco.pi_disco.controllers: + await async_stop_discovery_service(hass) + _LOGGER.debug("No controllers found") + return False + + _LOGGER.debug("Controllers %s", disco.pi_disco.controllers) + return True + + +config_entry_flow.register_discovery_flow( + IZONE, "iZone Aircon", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL +) diff --git a/homeassistant/components/izone/const.py b/homeassistant/components/izone/const.py new file mode 100644 index 00000000000000..4da7bc9e4afdb5 --- /dev/null +++ b/homeassistant/components/izone/const.py @@ -0,0 +1,14 @@ +"""Constants used by the izone component.""" + +IZONE = "izone" + +DATA_DISCOVERY_SERVICE = "izone_discovery" +DATA_CONFIG = "izone_config" + +DISPATCH_CONTROLLER_DISCOVERED = "izone_controller_discovered" +DISPATCH_CONTROLLER_DISCONNECTED = "izone_controller_disconnected" +DISPATCH_CONTROLLER_RECONNECTED = "izone_controller_disconnected" +DISPATCH_CONTROLLER_UPDATE = "izone_controller_update" +DISPATCH_ZONE_UPDATE = "izone_zone_update" + +TIMEOUT_DISCOVERY = 20 diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py new file mode 100644 index 00000000000000..3630c28605bb7f --- /dev/null +++ b/homeassistant/components/izone/discovery.py @@ -0,0 +1,87 @@ +"""Internal discovery service for iZone AC.""" + +import logging +import pizone + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ( + DATA_DISCOVERY_SERVICE, + DISPATCH_CONTROLLER_DISCOVERED, + DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_RECONNECTED, + DISPATCH_CONTROLLER_UPDATE, + DISPATCH_ZONE_UPDATE, +) + +_LOGGER = logging.getLogger(__name__) + + +class DiscoveryService(pizone.Listener): + """Discovery data and interfacing with pizone library.""" + + def __init__(self, hass): + """Initialise discovery service.""" + super().__init__() + self.hass = hass + self.pi_disco = None + + # Listener interface + def controller_discovered(self, ctrl: pizone.Controller) -> None: + """Handle new controller discoverery.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCOVERED, ctrl) + + def controller_disconnected(self, ctrl: pizone.Controller, ex: Exception) -> None: + """On disconnect from controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCONNECTED, ctrl, ex) + + def controller_reconnected(self, ctrl: pizone.Controller) -> None: + """On reconnect to controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_RECONNECTED, ctrl) + + def controller_update(self, ctrl: pizone.Controller) -> None: + """System update message is recieved from the controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_UPDATE, ctrl) + + def zone_update(self, ctrl: pizone.Controller, zone: pizone.Zone) -> None: + """Zone update message is recieved from the controller.""" + async_dispatcher_send(self.hass, DISPATCH_ZONE_UPDATE, ctrl, zone) + + +async def async_start_discovery_service(hass: HomeAssistantType): + """Set up the pizone internal discovery.""" + disco = hass.data.get(DATA_DISCOVERY_SERVICE) + if disco: + # Already started + return disco + + # discovery local services + disco = DiscoveryService(hass) + hass.data[DATA_DISCOVERY_SERVICE] = disco + + # Start the pizone discovery service, disco is the listener + session = aiohttp_client.async_get_clientsession(hass) + loop = hass.loop + + disco.pi_disco = pizone.discovery(disco, loop=loop, session=session) + await disco.pi_disco.start_discovery() + + async def shutdown_event(event): + await async_stop_discovery_service(hass) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_event) + + return disco + + +async def async_stop_discovery_service(hass: HomeAssistantType): + """Stop the discovery service.""" + disco = hass.data.get(DATA_DISCOVERY_SERVICE) + if not disco: + return + + await disco.pi_disco.close() + del hass.data[DATA_DISCOVERY_SERVICE] diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json new file mode 100644 index 00000000000000..2f6747ab4cc51d --- /dev/null +++ b/homeassistant/components/izone/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "izone", + "name": "izone", + "documentation": "https://www.home-assistant.io/components/izone", + "requirements": [ "python-izone==1.1.1" ], + "dependencies": [], + "codeowners": [ "@Swamp-Ig" ], + "config_flow": true +} diff --git a/homeassistant/components/izone/strings.json b/homeassistant/components/izone/strings.json new file mode 100644 index 00000000000000..7cb14b03c6c593 --- /dev/null +++ b/homeassistant/components/izone/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "title": "iZone", + "step": { + "confirm": { + "title": "iZone", + "description": "Do you want to set up iZone?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of iZone is necessary.", + "no_devices_found": "No iZone devices found on the network." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9ddae5acdb9941..9a534c01bbf2c6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -30,6 +30,7 @@ "ios", "ipma", "iqvia", + "izone", "life360", "lifx", "linky", diff --git a/requirements_all.txt b/requirements_all.txt index 99d81158edbae5..ae4007ee770a1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1508,6 +1508,9 @@ python-gitlab==1.6.0 # homeassistant.components.hp_ilo python-hpilo==4.3 +# homeassistant.components.izone +python-izone==1.1.1 + # homeassistant.components.joaoapps_join python-join-api==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d0d42cc8472bb..f9644058580088 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,6 +349,9 @@ pyspcwebgw==0.4.0 # homeassistant.components.darksky python-forecastio==1.4.0 +# homeassistant.components.izone +python-izone==1.1.1 + # homeassistant.components.nest python-nest==4.1.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 72fb9ff5a44233..384d50bccef729 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -145,6 +145,7 @@ "pyspcwebgw", "python_awair", "python-forecastio", + "python-izone", "python-nest", "python-velbus", "pythonwhois", diff --git a/tests/components/izone/__init__.py b/tests/components/izone/__init__.py new file mode 100644 index 00000000000000..1baeb3fee82702 --- /dev/null +++ b/tests/components/izone/__init__.py @@ -0,0 +1 @@ +"""IZone tests.""" diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py new file mode 100644 index 00000000000000..faa920271e385b --- /dev/null +++ b/tests/components/izone/test_config_flow.py @@ -0,0 +1,83 @@ +"""Tests for iZone.""" + +from unittest.mock import Mock, patch + +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.izone.const import IZONE, DISPATCH_CONTROLLER_DISCOVERED + +from tests.common import mock_coro + + +@pytest.fixture +def mock_disco(): + """Mock discovery service.""" + disco = Mock() + disco.pi_disco = Mock() + disco.pi_disco.controllers = {} + yield disco + + +def _mock_start_discovery(hass, mock_disco): + from homeassistant.helpers.dispatcher import async_dispatcher_send + + def do_disovered(*args): + async_dispatcher_send(hass, DISPATCH_CONTROLLER_DISCOVERED, True) + return mock_coro(mock_disco) + + return do_disovered + + +async def test_not_found(hass, mock_disco): + """Test not finding iZone controller.""" + + with patch( + "homeassistant.components.izone.discovery.async_start_discovery_service" + ) as start_disco, patch( + "homeassistant.components.izone.discovery.async_stop_discovery_service", + return_value=mock_coro(), + ) as stop_disco: + start_disco.side_effect = _mock_start_discovery(hass, mock_disco) + result = await hass.config_entries.flow.async_init( + IZONE, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + await hass.async_block_till_done() + + stop_disco.assert_called_once() + + +async def test_found(hass, mock_disco): + """Test not finding iZone controller.""" + mock_disco.pi_disco.controllers["blah"] = object() + + with patch( + "homeassistant.components.izone.climate.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.izone.discovery.async_start_discovery_service" + ) as start_disco, patch( + "homeassistant.components.izone.async_start_discovery_service", + return_value=mock_coro(), + ): + start_disco.side_effect = _mock_start_discovery(hass, mock_disco) + result = await hass.config_entries.flow.async_init( + IZONE, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + mock_setup.assert_called_once() From d26273a9e9392bd81e3e469feb5edc39683f082d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Sep 2019 23:38:58 +0200 Subject: [PATCH 076/296] Bump influxdb to 5.2.3 (#26743) --- homeassistant/components/influxdb/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index 20652ddd046375..feda5da732cab7 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -3,10 +3,10 @@ "name": "Influxdb", "documentation": "https://www.home-assistant.io/components/influxdb", "requirements": [ - "influxdb==5.2.0" + "influxdb==5.2.3" ], "dependencies": [], "codeowners": [ "@fabaff" ] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index ae4007ee770a1d..064762591db9a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -687,7 +687,7 @@ ihcsdk==2.3.0 incomfort-client==0.3.1 # homeassistant.components.influxdb -influxdb==5.2.0 +influxdb==5.2.3 # homeassistant.components.insteon insteonplm==0.16.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9644058580088..2f5a1bef6e469a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -197,7 +197,7 @@ huawei-lte-api==1.3.0 iaqualink==0.2.9 # homeassistant.components.influxdb -influxdb==5.2.0 +influxdb==5.2.3 # homeassistant.components.verisure jsonpath==0.75 From aac2c3e91cc9e9d15feea90a758d205819d8dfa0 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 19 Sep 2019 23:39:57 +0200 Subject: [PATCH 077/296] Update codeowners (#26733) --- CODEOWNERS | 5 ----- homeassistant/components/lifx/manifest.json | 4 +--- homeassistant/components/lifx_cloud/manifest.json | 4 +--- homeassistant/components/lifx_legacy/manifest.json | 4 +--- homeassistant/components/netgear_lte/manifest.json | 4 +--- homeassistant/components/sonos/manifest.json | 4 +--- 6 files changed, 5 insertions(+), 20 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 19dd0d5c8b69f3..9766f011889fe9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -154,9 +154,6 @@ homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus homeassistant/components/life360/* @pnbruckner -homeassistant/components/lifx/* @amelchio -homeassistant/components/lifx_cloud/* @amelchio -homeassistant/components/lifx_legacy/* @amelchio homeassistant/components/linky/* @Quentame homeassistant/components/linux_battery/* @fabaff homeassistant/components/liveboxplaytv/* @pschmitt @@ -187,7 +184,6 @@ homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan homeassistant/components/netdata/* @fabaff -homeassistant/components/netgear_lte/* @amelchio homeassistant/components/nextbus/* @vividboarder homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek @@ -254,7 +250,6 @@ homeassistant/components/solaredge_local/* @drobtravels homeassistant/components/solax/* @squishykid homeassistant/components/somfy/* @tetienne homeassistant/components/songpal/* @rytilahti -homeassistant/components/sonos/* @amelchio homeassistant/components/spaceapi/* @fabaff homeassistant/components/spider/* @peternijssen homeassistant/components/sql/* @dgomes diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index fd74d9831fca0a..131d1a23b6a5f3 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -13,7 +13,5 @@ ] }, "dependencies": [], - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/lifx_cloud/manifest.json b/homeassistant/components/lifx_cloud/manifest.json index c2834fbc788b63..83805692e4d246 100644 --- a/homeassistant/components/lifx_cloud/manifest.json +++ b/homeassistant/components/lifx_cloud/manifest.json @@ -4,7 +4,5 @@ "documentation": "https://www.home-assistant.io/components/lifx_cloud", "requirements": [], "dependencies": [], - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/lifx_legacy/manifest.json b/homeassistant/components/lifx_legacy/manifest.json index 4ff59ac17703df..fb38b41f314c4a 100644 --- a/homeassistant/components/lifx_legacy/manifest.json +++ b/homeassistant/components/lifx_legacy/manifest.json @@ -6,7 +6,5 @@ "liffylights==0.9.4" ], "dependencies": [], - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 609ea72cc699c3..99ca3cb1ccfb82 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -6,7 +6,5 @@ "eternalegypt==0.0.10" ], "dependencies": [], - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 8c231ec63e0914..a08c0a59c07fd6 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -12,7 +12,5 @@ "urn:schemas-upnp-org:device:ZonePlayer:1" ] }, - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } From 9e2cd5116acee97cc09453e564e419371e8e3e9c Mon Sep 17 00:00:00 2001 From: Askarov Rishat Date: Fri, 20 Sep 2019 00:41:44 +0300 Subject: [PATCH 078/296] Add transport data from maps.yandex.ru api (#26252) * adding feature obtaining Moscow transport data from maps.yandex.ru api * extracting the YandexMapsRequester to pypi * fix code review comments * fix stop_name, state in datetime, logger formating * fix comments * add docstring to init * rename, because it works not only Moscow, but many another big cities in Russia * fix comments * Try to solve relative view in sensor timestamp * back to isoformat * add tests, update external library version * flake8 and black tests for sensor.py * fix manifest.json * update tests, migrate to pytest, async, Using MockDependency * move json to tests/fixtures * script/lint fixes * fix comments * removing check_filter function * fix typo --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/yandex_transport/__init__.py | 1 + .../components/yandex_transport/manifest.json | 12 + .../components/yandex_transport/sensor.py | 128 + requirements_all.txt | 3 + tests/components/yandex_transport/__init__.py | 1 + .../test_yandex_transport_sensor.py | 88 + tests/fixtures/yandex_transport_reply.json | 2106 +++++++++++++++++ 9 files changed, 2341 insertions(+) create mode 100644 homeassistant/components/yandex_transport/__init__.py create mode 100644 homeassistant/components/yandex_transport/manifest.json create mode 100644 homeassistant/components/yandex_transport/sensor.py create mode 100644 tests/components/yandex_transport/__init__.py create mode 100644 tests/components/yandex_transport/test_yandex_transport_sensor.py create mode 100644 tests/fixtures/yandex_transport_reply.json diff --git a/.coveragerc b/.coveragerc index 5d5b0f6c81b09b..a29586c7b6e1cd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -747,6 +747,7 @@ omit = homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yamaha/media_player.py homeassistant/components/yamaha_musiccast/media_player.py + homeassistant/components/yandex_transport/* homeassistant/components/yeelight/* homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py diff --git a/CODEOWNERS b/CODEOWNERS index 9766f011889fe9..9bcd475d5d4788 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -318,6 +318,7 @@ homeassistant/components/xiaomi_miio/* @rytilahti @syssi homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yamaha_musiccast/* @jalmeroth +homeassistant/components/yandex_transport/* @rishatik92 homeassistant/components/yeelight/* @rytilahti @zewelor homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yessssms/* @flowolf diff --git a/homeassistant/components/yandex_transport/__init__.py b/homeassistant/components/yandex_transport/__init__.py new file mode 100644 index 00000000000000..d007b2d3df8581 --- /dev/null +++ b/homeassistant/components/yandex_transport/__init__.py @@ -0,0 +1 @@ +"""Service for obtaining information about closer bus from Transport Yandex Service.""" diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json new file mode 100644 index 00000000000000..54837b2eb0914d --- /dev/null +++ b/homeassistant/components/yandex_transport/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "yandex_transport", + "name": "Yandex Transport", + "documentation": "https://www.home-assistant.io/components/yandex_transport", + "requirements": [ + "ya_ma==0.3.4" + ], + "dependencies": [], + "codeowners": [ + "@rishatik92" + ] +} diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py new file mode 100644 index 00000000000000..340291807ead98 --- /dev/null +++ b/homeassistant/components/yandex_transport/sensor.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +"""Service for obtaining information about closer bus from Transport Yandex Service.""" + +import logging +from datetime import timedelta + +import voluptuous as vol +from ya_ma import YandexMapsRequester + +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +STOP_NAME = "stop_name" +USER_AGENT = "Home Assistant" +ATTRIBUTION = "Data provided by maps.yandex.ru" + +CONF_STOP_ID = "stop_id" +CONF_ROUTE = "routes" + +DEFAULT_NAME = "Yandex Transport" +ICON = "mdi:bus" + +SCAN_INTERVAL = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_STOP_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ROUTE, default=[]): vol.All(cv.ensure_list, [cv.string]), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Yandex transport sensor.""" + stop_id = config[CONF_STOP_ID] + name = config[CONF_NAME] + routes = config[CONF_ROUTE] + + data = YandexMapsRequester(user_agent=USER_AGENT) + add_entities([DiscoverMoscowYandexTransport(data, stop_id, routes, name)], True) + + +class DiscoverMoscowYandexTransport(Entity): + """Implementation of yandex_transport sensor.""" + + def __init__(self, requester, stop_id, routes, name): + """Initialize sensor.""" + self.requester = requester + self._stop_id = stop_id + self._routes = [] + self._routes = routes + self._state = None + self._name = name + self._attrs = None + + def update(self): + """Get the latest data from maps.yandex.ru and update the states.""" + attrs = {} + closer_time = None + try: + yandex_reply = self.requester.get_stop_info(self._stop_id) + data = yandex_reply["data"] + stop_metadata = data["properties"]["StopMetaData"] + except KeyError as key_error: + _LOGGER.warning( + "Exception KeyError was captured, missing key is %s. Yandex returned: %s", + key_error, + yandex_reply, + ) + self.requester.set_new_session() + data = self.requester.get_stop_info(self._stop_id)["data"] + stop_metadata = data["properties"]["StopMetaData"] + stop_name = data["properties"]["name"] + transport_list = stop_metadata["Transport"] + for transport in transport_list: + route = transport["name"] + if self._routes and route not in self._routes: + # skip unnecessary route info + continue + if "Events" in transport["BriefSchedule"]: + for event in transport["BriefSchedule"]["Events"]: + if "Estimated" in event: + posix_time_next = int(event["Estimated"]["value"]) + if closer_time is None or closer_time > posix_time_next: + closer_time = posix_time_next + if route not in attrs: + attrs[route] = [] + attrs[route].append(event["Estimated"]["text"]) + attrs[STOP_NAME] = stop_name + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION + if closer_time is None: + self._state = None + else: + self._state = dt_util.utc_from_timestamp(closer_time).isoformat( + timespec="seconds" + ) + self._attrs = attrs + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 064762591db9a2..437aa60cf96d15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1994,6 +1994,9 @@ xmltodict==0.12.0 # homeassistant.components.xs1 xs1-api-client==2.3.5 +# homeassistant.components.yandex_transport +ya_ma==0.3.4 + # homeassistant.components.yweather yahooweather==0.10 diff --git a/tests/components/yandex_transport/__init__.py b/tests/components/yandex_transport/__init__.py new file mode 100644 index 00000000000000..fe6b0db52d3e05 --- /dev/null +++ b/tests/components/yandex_transport/__init__.py @@ -0,0 +1 @@ +"""Tests for the yandex transport platform.""" diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py new file mode 100644 index 00000000000000..50d945e7fae371 --- /dev/null +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -0,0 +1,88 @@ +"""Tests for the yandex transport platform.""" + +import json +import pytest + +import homeassistant.components.sensor as sensor +import homeassistant.util.dt as dt_util +from homeassistant.const import CONF_NAME +from tests.common import ( + assert_setup_component, + async_setup_component, + MockDependency, + load_fixture, +) + +REPLY = json.loads(load_fixture("yandex_transport_reply.json")) + + +@pytest.fixture +def mock_requester(): + """Create a mock ya_ma module and YandexMapsRequester.""" + with MockDependency("ya_ma") as ya_ma: + instance = ya_ma.YandexMapsRequester.return_value + instance.get_stop_info.return_value = REPLY + yield instance + + +STOP_ID = 9639579 +ROUTES = ["194", "т36", "т47", "м10"] +NAME = "test_name" +TEST_CONFIG = { + "sensor": { + "platform": "yandex_transport", + "stop_id": 9639579, + "routes": ROUTES, + "name": NAME, + } +} + +FILTERED_ATTRS = { + "т36": ["21:43", "21:47", "22:02"], + "т47": ["21:40", "22:01"], + "м10": ["21:48", "22:00"], + "stop_name": "7-й автобусный парк", + "attribution": "Data provided by maps.yandex.ru", +} + +RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds") + + +async def assert_setup_sensor(hass, config, count=1): + """Set up the sensor and assert it's been created.""" + with assert_setup_component(count): + assert await async_setup_component(hass, sensor.DOMAIN, config) + + +async def test_setup_platform_valid_config(hass, mock_requester): + """Test that sensor is set up properly with valid config.""" + await assert_setup_sensor(hass, TEST_CONFIG) + + +async def test_setup_platform_invalid_config(hass, mock_requester): + """Check an invalid configuration.""" + await assert_setup_sensor( + hass, {"sensor": {"platform": "yandex_transport", "stopid": 1234}}, count=0 + ) + + +async def test_name(hass, mock_requester): + """Return the name if set in the configuration.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + assert state.name == TEST_CONFIG["sensor"][CONF_NAME] + + +async def test_state(hass, mock_requester): + """Return the contents of _state.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + assert state.state == RESULT_STATE + + +async def test_filtered_attributes(hass, mock_requester): + """Return the contents of attributes.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + state_attrs = {key: state.attributes[key] for key in FILTERED_ATTRS} + assert state_attrs == FILTERED_ATTRS diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_reply.json new file mode 100644 index 00000000000000..c5e4857297aa93 --- /dev/null +++ b/tests/fixtures/yandex_transport_reply.json @@ -0,0 +1,2106 @@ +{ + "data": { + "geometries": [ + { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + } + ], + "geometry": { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + }, + "properties": { + "name": "7-й автобусный парк", + "description": "7-й автобусный парк", + "currentTime": "Mon Sep 16 2019 21:40:40 GMT+0300 (Moscow Standard Time)", + "StopMetaData": { + "id": "stop__9639579", + "name": "7-й автобусный парк", + "type": "urban", + "region": { + "id": 213, + "type": 6, + "parent_id": 1, + "capital_id": 0, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow", + "native_name": "", + "iso_name": "RU MOW", + "is_main": true, + "en_name": "Moscow", + "short_en_name": "MSK", + "phone_code": "495 499", + "phone_code_old": "095", + "zip_code": "", + "population": 12506468, + "synonyms": "Moskau, Moskva", + "latitude": 55.753215, + "longitude": 37.622504, + "latitude_size": 0.878654, + "longitude_size": 1.164423, + "zoom": 10, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "weather", + "afisha", + "maps", + "tv", + "ad", + "etrain", + "subway", + "delivery", + "route" + ], + "ename": "moscow", + "bounds": [ + [ + 37.0402925, + 55.31141404514547 + ], + [ + 38.2047155, + 56.190068045145466 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву", + "dative": "Москве", + "directional": "", + "genitive": "Москвы", + "instrumental": "Москвой", + "locative": "", + "nominative": "Москва", + "preposition": "в", + "prepositional": "Москве" + }, + "parent": { + "id": 1, + "type": 5, + "parent_id": 3, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow-and-moscow-oblast", + "native_name": "", + "iso_name": "RU-MOS", + "is_main": true, + "en_name": "Moscow and Moscow Oblast", + "short_en_name": "RU-MOS", + "phone_code": "495 496 498 499", + "phone_code_old": "", + "zip_code": "", + "population": 7503385, + "synonyms": "Московская область, Подмосковье, Podmoskovye", + "latitude": 55.815792, + "longitude": 37.380031, + "latitude_size": 2.705659, + "longitude_size": 5.060749, + "zoom": 8, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 10716, + 10747, + 10758, + 20728, + 10740, + 10738, + 20523, + 10735, + 10734, + 10743, + 21622 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "moscow-and-moscow-oblast", + "bounds": [ + [ + 34.8496565, + 54.439456064325434 + ], + [ + 39.9104055, + 57.14511506432543 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву и Московскую область", + "dative": "Москве и Московской области", + "directional": "", + "genitive": "Москвы и Московской области", + "instrumental": "Москвой и Московской областью", + "locative": "", + "nominative": "Москва и Московская область", + "preposition": "в", + "prepositional": "Москве и Московской области" + }, + "parent": { + "id": 225, + "type": 3, + "parent_id": 10001, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "russia", + "native_name": "", + "iso_name": "RU", + "is_main": false, + "en_name": "Russia", + "short_en_name": "RU", + "phone_code": "7", + "phone_code_old": "", + "zip_code": "", + "population": 146880432, + "synonyms": "Russian Federation,Российская Федерация", + "latitude": 61.698653, + "longitude": 99.505405, + "latitude_size": 40.700127, + "longitude_size": 171.643239, + "zoom": 3, + "tzname": "", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 2, + 65, + 54, + 47, + 43, + 66, + 51, + 56, + 172, + 39, + 62 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "russia", + "bounds": [ + [ + 13.683785499999999, + 35.290400699917846 + ], + [ + -174.6729755, + 75.99052769991785 + ] + ], + "names": { + "ablative": "", + "accusative": "Россию", + "dative": "России", + "directional": "", + "genitive": "России", + "instrumental": "Россией", + "locative": "", + "nominative": "Россия", + "preposition": "в", + "prepositional": "России" + } + } + } + }, + "Transport": [ + { + "lineId": "2036925416", + "name": "194", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659860", + "tzOffset": 10800, + "text": "21:51" + } + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661840", + "tzOffset": 10800, + "text": "22:24" + } + } + ], + "departureTime": "21:51" + } + } + ], + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659860", + "tzOffset": 10800, + "text": "21:51" + } + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661840", + "tzOffset": 10800, + "text": "22:24" + } + } + ], + "departureTime": "21:51" + } + }, + { + "lineId": "213_114_bus_mosgortrans", + "name": "114", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568603405", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1568672165", + "tzOffset": 10800, + "text": "1:16" + } + } + } + } + ], + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568603405", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1568672165", + "tzOffset": 10800, + "text": "1:16" + } + } + } + }, + { + "lineId": "213_154_bus_mosgortrans", + "name": "154", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659260", + "tzOffset": 10800, + "text": "21:41" + }, + "Estimated": { + "value": "1568659252", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1054764%5F191500" + }, + { + "Scheduled": { + "value": "1568660580", + "tzOffset": 10800, + "text": "22:03" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:41" + } + } + ], + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659260", + "tzOffset": 10800, + "text": "21:41" + }, + "Estimated": { + "value": "1568659252", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1054764%5F191500" + }, + { + "Scheduled": { + "value": "1568660580", + "tzOffset": 10800, + "text": "22:03" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:41" + } + }, + { + "lineId": "213_179_bus_mosgortrans", + "name": "179", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568659351", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|59832%5F31359" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661660", + "tzOffset": 10800, + "text": "22:21" + } + } + ], + "departureTime": "21:52" + } + } + ], + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568659351", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|59832%5F31359" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661660", + "tzOffset": 10800, + "text": "22:21" + } + } + ], + "departureTime": "21:52" + } + }, + { + "lineId": "213_191m_minibus_default", + "name": "591", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660525", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|38278%5F9345312" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568602033", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1568672233", + "tzOffset": 10800, + "text": "1:17" + } + } + } + } + ], + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660525", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|38278%5F9345312" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568602033", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1568672233", + "tzOffset": 10800, + "text": "1:17" + } + } + } + }, + { + "lineId": "213_206m_minibus_default", + "name": "206к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568601239", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1568671439", + "tzOffset": 10800, + "text": "1:03" + } + } + } + } + ], + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568601239", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1568671439", + "tzOffset": 10800, + "text": "1:03" + } + } + } + }, + { + "lineId": "213_215_bus_mosgortrans", + "name": "215", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1568601276", + "tzOffset": 10800, + "text": "5:34" + }, + "end": { + "value": "1568671476", + "tzOffset": 10800, + "text": "1:04" + } + } + } + } + ], + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1568601276", + "tzOffset": 10800, + "text": "5:34" + }, + "end": { + "value": "1568671476", + "tzOffset": 10800, + "text": "1:04" + } + } + } + }, + { + "lineId": "213_282_bus_mosgortrans", + "name": "282", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659888", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|34874%5F9345408" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568602180", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1568673460", + "tzOffset": 10800, + "text": "1:37" + } + } + } + } + ], + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659888", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|34874%5F9345408" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568602180", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1568673460", + "tzOffset": 10800, + "text": "1:37" + } + } + } + }, + { + "lineId": "213_294m_minibus_default", + "name": "994", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1568601527", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1568671727", + "tzOffset": 10800, + "text": "1:08" + } + } + } + } + ], + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1568601527", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1568671727", + "tzOffset": 10800, + "text": "1:08" + } + } + } + }, + { + "lineId": "213_36_trolleybus_mosgortrans", + "name": "т36", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + }, + "Estimated": { + "value": "1568659426", + "tzOffset": 10800, + "text": "21:43" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" + }, + { + "Scheduled": { + "value": "1568660520", + "tzOffset": 10800, + "text": "22:02" + }, + "Estimated": { + "value": "1568659656", + "tzOffset": 10800, + "text": "21:47" + }, + "vehicleId": "codd%5Fnew|1117016%5F430280" + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + }, + "Estimated": { + "value": "1568660538", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|1054576%5F430226" + } + ], + "departureTime": "21:48" + } + } + ], + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + }, + "Estimated": { + "value": "1568659426", + "tzOffset": 10800, + "text": "21:43" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" + }, + { + "Scheduled": { + "value": "1568660520", + "tzOffset": 10800, + "text": "22:02" + }, + "Estimated": { + "value": "1568659656", + "tzOffset": 10800, + "text": "21:47" + }, + "vehicleId": "codd%5Fnew|1117016%5F430280" + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + }, + "Estimated": { + "value": "1568660538", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|1054576%5F430226" + } + ], + "departureTime": "21:48" + } + }, + { + "lineId": "213_47_trolleybus_mosgortrans", + "name": "т47", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + }, + "Estimated": { + "value": "1568659253", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1112219%5F430329" + }, + { + "Scheduled": { + "value": "1568660940", + "tzOffset": 10800, + "text": "22:09" + }, + "Estimated": { + "value": "1568660519", + "tzOffset": 10800, + "text": "22:01" + }, + "vehicleId": "codd%5Fnew|1139620%5F430382" + }, + { + "Scheduled": { + "value": "1568663580", + "tzOffset": 10800, + "text": "22:53" + } + } + ], + "departureTime": "21:53" + } + } + ], + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + }, + "Estimated": { + "value": "1568659253", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1112219%5F430329" + }, + { + "Scheduled": { + "value": "1568660940", + "tzOffset": 10800, + "text": "22:09" + }, + "Estimated": { + "value": "1568660519", + "tzOffset": 10800, + "text": "22:01" + }, + "vehicleId": "codd%5Fnew|1139620%5F430382" + }, + { + "Scheduled": { + "value": "1568663580", + "tzOffset": 10800, + "text": "22:53" + } + } + ], + "departureTime": "21:53" + } + }, + { + "lineId": "213_56_trolleybus_mosgortrans", + "name": "т56", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660675", + "tzOffset": 10800, + "text": "22:04" + }, + "vehicleId": "codd%5Fnew|146304%5F31207" + } + ], + "Frequency": { + "text": "8 мин", + "value": 480, + "begin": { + "value": "1568606244", + "tzOffset": 10800, + "text": "6:57" + }, + "end": { + "value": "1568670144", + "tzOffset": 10800, + "text": "0:42" + } + } + } + } + ], + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660675", + "tzOffset": 10800, + "text": "22:04" + }, + "vehicleId": "codd%5Fnew|146304%5F31207" + } + ], + "Frequency": { + "text": "8 мин", + "value": 480, + "begin": { + "value": "1568606244", + "tzOffset": 10800, + "text": "6:57" + }, + "end": { + "value": "1568670144", + "tzOffset": 10800, + "text": "0:42" + } + } + } + }, + { + "lineId": "213_63_bus_mosgortrans", + "name": "63", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|38921%5F9215306" + }, + { + "Estimated": { + "value": "1568660136", + "tzOffset": 10800, + "text": "21:55" + }, + "vehicleId": "codd%5Fnew|38918%5F9215303" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1568600987", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568670227", + "tzOffset": 10800, + "text": "0:43" + } + } + } + } + ], + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|38921%5F9215306" + }, + { + "Estimated": { + "value": "1568660136", + "tzOffset": 10800, + "text": "21:55" + }, + "vehicleId": "codd%5Fnew|38918%5F9215303" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1568600987", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568670227", + "tzOffset": 10800, + "text": "0:43" + } + } + } + }, + { + "lineId": "213_677_bus_mosgortrans", + "name": "677", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|11731%5F31376" + } + ], + "Frequency": { + "text": "4 мин", + "value": 240, + "begin": { + "value": "1568600940", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568672640", + "tzOffset": 10800, + "text": "1:24" + } + } + } + } + ], + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|11731%5F31376" + } + ], + "Frequency": { + "text": "4 мин", + "value": 240, + "begin": { + "value": "1568600940", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568672640", + "tzOffset": 10800, + "text": "1:24" + } + } + } + }, + { + "lineId": "213_692_bus_mosgortrans", + "name": "692", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568660280", + "tzOffset": 10800, + "text": "21:58" + }, + "Estimated": { + "value": "1568660255", + "tzOffset": 10800, + "text": "21:57" + }, + "vehicleId": "codd%5Fnew|63029%5F31485" + }, + { + "Scheduled": { + "value": "1568693340", + "tzOffset": 10800, + "text": "7:09" + } + }, + { + "Scheduled": { + "value": "1568696940", + "tzOffset": 10800, + "text": "8:09" + } + } + ], + "departureTime": "21:58" + } + } + ], + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568660280", + "tzOffset": 10800, + "text": "21:58" + }, + "Estimated": { + "value": "1568660255", + "tzOffset": 10800, + "text": "21:57" + }, + "vehicleId": "codd%5Fnew|63029%5F31485" + }, + { + "Scheduled": { + "value": "1568693340", + "tzOffset": 10800, + "text": "7:09" + } + }, + { + "Scheduled": { + "value": "1568696940", + "tzOffset": 10800, + "text": "8:09" + } + } + ], + "departureTime": "21:58" + } + }, + { + "lineId": "213_78_trolleybus_mosgortrans", + "name": "т78", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659620", + "tzOffset": 10800, + "text": "21:47" + }, + "Estimated": { + "value": "1568659898", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|147522%5F31184" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:47" + } + } + ], + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659620", + "tzOffset": 10800, + "text": "21:47" + }, + "Estimated": { + "value": "1568659898", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|147522%5F31184" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:47" + } + }, + { + "lineId": "213_82_bus_mosgortrans", + "name": "82", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + } + }, + { + "Scheduled": { + "value": "1568661780", + "tzOffset": 10800, + "text": "22:23" + } + }, + { + "Scheduled": { + "value": "1568663760", + "tzOffset": 10800, + "text": "22:56" + } + } + ], + "departureTime": "21:48" + } + } + ], + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + } + }, + { + "Scheduled": { + "value": "1568661780", + "tzOffset": 10800, + "text": "22:23" + } + }, + { + "Scheduled": { + "value": "1568663760", + "tzOffset": 10800, + "text": "22:56" + } + } + ], + "departureTime": "21:48" + } + }, + { + "lineId": "2465131598", + "name": "179к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659500", + "tzOffset": 10800, + "text": "21:45" + } + }, + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + } + }, + { + "Scheduled": { + "value": "1568660880", + "tzOffset": 10800, + "text": "22:08" + } + } + ], + "departureTime": "21:45" + } + } + ], + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659500", + "tzOffset": 10800, + "text": "21:45" + } + }, + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + } + }, + { + "Scheduled": { + "value": "1568660880", + "tzOffset": 10800, + "text": "22:08" + } + } + ], + "departureTime": "21:45" + } + }, + { + "lineId": "466_bus_default", + "name": "466", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568604647", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1568675447", + "tzOffset": 10800, + "text": "2:10" + } + } + } + } + ], + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568604647", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1568675447", + "tzOffset": 10800, + "text": "2:10" + } + } + } + }, + { + "lineId": "677k_bus_default", + "name": "677к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568660003", + "tzOffset": 10800, + "text": "21:53" + }, + "vehicleId": "codd%5Fnew|130308%5F31319" + }, + { + "Scheduled": { + "value": "1568661240", + "tzOffset": 10800, + "text": "22:14" + } + }, + { + "Scheduled": { + "value": "1568662500", + "tzOffset": 10800, + "text": "22:35" + } + } + ], + "departureTime": "21:52" + } + } + ], + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568660003", + "tzOffset": 10800, + "text": "21:53" + }, + "vehicleId": "codd%5Fnew|130308%5F31319" + }, + { + "Scheduled": { + "value": "1568661240", + "tzOffset": 10800, + "text": "22:14" + } + }, + { + "Scheduled": { + "value": "1568662500", + "tzOffset": 10800, + "text": "22:35" + } + } + ], + "departureTime": "21:52" + } + }, + { + "lineId": "m10_bus_default", + "name": "м10", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659718", + "tzOffset": 10800, + "text": "21:48" + }, + "vehicleId": "codd%5Fnew|146260%5F31212" + }, + { + "Estimated": { + "value": "1568660422", + "tzOffset": 10800, + "text": "22:00" + }, + "vehicleId": "codd%5Fnew|13997%5F31247" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568606903", + "tzOffset": 10800, + "text": "7:08" + }, + "end": { + "value": "1568675183", + "tzOffset": 10800, + "text": "2:06" + } + } + } + } + ], + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659718", + "tzOffset": 10800, + "text": "21:48" + }, + "vehicleId": "codd%5Fnew|146260%5F31212" + }, + { + "Estimated": { + "value": "1568660422", + "tzOffset": 10800, + "text": "22:00" + }, + "vehicleId": "codd%5Fnew|13997%5F31247" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568606903", + "tzOffset": 10800, + "text": "7:08" + }, + "end": { + "value": "1568675183", + "tzOffset": 10800, + "text": "2:06" + } + } + } + } + ] + } + }, + "toponymSeoname": "dmitrovskoye_shosse" + } +} From f5d12669a504bf0ca8b5038e4644c853642581a8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 19 Sep 2019 23:44:09 +0200 Subject: [PATCH 079/296] deCONZ improve gateway tests (#26709) * Improve gateway tests * Harmonize all tests to use the same gateway initialization method * Improve scene tests * Add gateway resync call to platform tests * Forgot to change switch tests to use common gateway method * Improve event tests --- homeassistant/components/deconz/gateway.py | 8 +- homeassistant/components/deconz/scene.py | 1 - tests/components/deconz/test_binary_sensor.py | 52 +--- tests/components/deconz/test_climate.py | 51 +-- tests/components/deconz/test_cover.py | 51 +-- tests/components/deconz/test_deconz_event.py | 120 +++---- tests/components/deconz/test_gateway.py | 293 +++++++++--------- tests/components/deconz/test_light.py | 51 +-- tests/components/deconz/test_scene.py | 93 ++---- tests/components/deconz/test_sensor.py | 52 +--- tests/components/deconz/test_services.py | 82 ++--- tests/components/deconz/test_switch.py | 52 +--- 12 files changed, 313 insertions(+), 593 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index a090dca0d0c632..75898b0fdab819 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -184,11 +184,7 @@ def shutdown(self, event): self.api.close() async def async_reset(self): - """Reset this gateway to default state. - - Will cancel any scheduled setup retry and will unload - the config entry. - """ + """Reset this gateway to default state.""" self.api.async_connection_status_callback = None self.api.close() @@ -203,7 +199,7 @@ async def async_reset(self): for event in self.events: event.async_will_remove_from_hass() - self.events.remove(event) + self.events.clear() self.deconz_ids = {} return True diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 8d27d386da266b..a84e799d44d47b 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -9,7 +9,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index c5c35f108296b0..2f42193291c8dd 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -1,14 +1,12 @@ """deCONZ binary sensor platform tests.""" from copy import deepcopy -from asynctest import patch - -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component import homeassistant.components.binary_sensor as binary_sensor +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration SENSORS = { "1": { @@ -50,50 +48,6 @@ }, } -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - - -async def setup_deconz_integration(hass, config, options, get_state_response): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response - ), patch("pydeconz.DeconzSession.start", return_value=True): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" @@ -147,6 +101,10 @@ async def test_binary_sensors(hass): presence_sensor = hass.states.get("binary_sensor.presence_sensor") assert presence_sensor.state == "on" + await gateway.async_reset() + + assert len(hass.states.async_all()) == 0 + async def test_allow_clip_sensor(hass): """Test that CLIP sensors can be allowed.""" diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 1211188d3db3fa..cee91f00c4283b 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -3,12 +3,13 @@ from asynctest import patch -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component import homeassistant.components.climate as climate +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration + SENSORS = { "1": { "id": "Thermostat id", @@ -42,50 +43,6 @@ }, } -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - - -async def setup_deconz_integration(hass, config, options, get_state_response): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response - ), patch("pydeconz.DeconzSession.start", return_value=True): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" @@ -205,6 +162,10 @@ async def test_climate_devices(hass): ) set_callback.assert_called_with("/sensors/1/config", {"heatsetpoint": 2000.0}) + await gateway.async_reset() + + assert len(hass.states.async_all()) == 0 + async def test_clip_climate_device(hass): """Test successful creation of sensor entities.""" diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 246c2bae7c561c..5c7ee48a78a2eb 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -3,12 +3,13 @@ from asynctest import patch -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component import homeassistant.components.cover as cover +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration + COVERS = { "1": { "id": "Level controllable cover id", @@ -35,50 +36,6 @@ }, } -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - - -async def setup_deconz_integration(hass, config, options, get_state_response): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response - ), patch("pydeconz.DeconzSession.start", return_value=True): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" @@ -159,3 +116,7 @@ async def test_cover(hass): ) await hass.async_block_till_done() set_callback.assert_called_with("/lights/1/state", {"bri_inc": 0}) + + await gateway.async_reset() + + assert len(hass.states.async_all()) == 2 diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 72966ba6c66b09..ade9aa02ad4afb 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -1,60 +1,74 @@ """Test deCONZ remote events.""" -from unittest.mock import Mock - -from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT, DeconzEvent -from homeassistant.core import callback - - -async def test_create_event(hass): - """Successfully created a deCONZ event.""" - mock_remote = Mock() - mock_remote.name = "Name" - - mock_gateway = Mock() - mock_gateway.hass = hass - - event = DeconzEvent(mock_remote, mock_gateway) - - assert event.event_id == "name" - - -async def test_update_event(hass): - """Successfully update a deCONZ event.""" - mock_remote = Mock() - mock_remote.name = "Name" - - mock_gateway = Mock() - mock_gateway.hass = hass - - event = DeconzEvent(mock_remote, mock_gateway) - mock_remote.changed_keys = {"state": True} - - calls = [] - - @callback - def listener(event): - """Mock listener.""" - calls.append(event) - - unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, listener) - - event.async_update_callback() +from copy import deepcopy + +from asynctest import Mock + +from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT + +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration + +SENSORS = { + "1": { + "id": "Switch 1 id", + "name": "Switch 1", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "2": { + "id": "Switch 2 id", + "name": "Switch 2", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {"battery": 100}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, +} + + +async def test_deconz_events(hass): + """Test successful creation of deconz events.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert "sensor.switch_1" not in gateway.deconz_ids + assert "sensor.switch_1_battery_level" not in gateway.deconz_ids + assert "sensor.switch_2" not in gateway.deconz_ids + assert "sensor.switch_2_battery_level" in gateway.deconz_ids + assert len(hass.states.async_all()) == 1 + assert len(gateway.events) == 2 + + switch_1 = hass.states.get("sensor.switch_1") + assert switch_1 is None + + switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level") + assert switch_1_battery_level is None + + switch_2 = hass.states.get("sensor.switch_2") + assert switch_2 is None + + switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") + assert switch_2_battery_level.state == "100" + + mock_listener = Mock() + unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) + + gateway.api.sensors["1"].async_update({"state": {"buttonevent": 2000}}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(mock_listener.mock_calls) == 1 + assert mock_listener.mock_calls[0][1][0].data == { + "id": "switch_1", + "unique_id": "00:00:00:00:00:00:00:01", + "event": 2000, + } unsub() + await gateway.async_reset() -async def test_remove_event(hass): - """Successfully update a deCONZ event.""" - mock_remote = Mock() - mock_remote.name = "Name" - - mock_gateway = Mock() - mock_gateway.hass = hass - - event = DeconzEvent(mock_remote, mock_gateway) - event.async_will_remove_from_hass() - - assert event._device is None + assert len(hass.states.async_all()) == 0 + assert len(gateway.events) == 0 diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index d84706430f465b..25a1cd465c510a 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -1,187 +1,178 @@ """Test deCONZ gateway.""" -from unittest.mock import Mock, patch +from copy import deepcopy + +from asynctest import Mock, patch import pytest +from homeassistant import config_entries +from homeassistant.components import deconz +from homeassistant.components import ssdp from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.components.deconz import errors, gateway - -from tests.common import mock_coro +from homeassistant.helpers.dispatcher import async_dispatcher_connect import pydeconz +BRIDGEID = "0123456789" ENTRY_CONFIG = { - "host": "1.2.3.4", - "port": 80, - "api_key": "1234567890ABCDEF", - "bridgeid": "0123456789ABCDEF", - "allow_clip_sensor": True, - "allow_deconz_groups": True, + deconz.config_flow.CONF_API_KEY: "ABCDEF", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, + deconz.config_flow.CONF_HOST: "1.2.3.4", + deconz.config_flow.CONF_PORT: 80, } +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "ipaddress": "1.2.3.4", + "mac": "00:11:22:33:44:55", + "modelid": "deCONZ", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "uuid": "1234", + "websocketport": 1234, +} -async def test_gateway_setup(): - """Successful setup.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - api = Mock() - api.async_add_remote.return_value = Mock() - api.sensors = {} - - deconz_gateway = gateway.DeconzGateway(hass, entry) - - with patch.object( - gateway, "get_gateway", return_value=mock_coro(api) - ), patch.object(gateway, "async_dispatcher_connect", return_value=Mock()): - assert await deconz_gateway.async_setup() is True - - assert deconz_gateway.api is api - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 7 - assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == ( - entry, - "binary_sensor", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == ( - entry, - "climate", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[2][1] == ( - entry, - "cover", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[3][1] == ( - entry, - "light", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[4][1] == ( - entry, - "scene", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[5][1] == ( - entry, - "sensor", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[6][1] == ( - entry, - "switch", +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} + + +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" + config_entry = config_entries.ConfigEntry( + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, + options=options, + entry_id="1", ) - assert len(api.start.mock_calls) == 1 + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() -async def test_gateway_retry(): - """Retry setup.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG + hass.config_entries._entries.append(config_entry) - deconz_gateway = gateway.DeconzGateway(hass, entry) + return hass.data[deconz.DOMAIN].get(config[deconz.CONF_BRIDGEID]) - with patch.object( - gateway, "get_gateway", side_effect=errors.CannotConnect - ), pytest.raises(ConfigEntryNotReady): - await deconz_gateway.async_setup() - -async def test_gateway_setup_fails(): +async def test_gateway_setup(hass): + """Successful setup.""" + data = deepcopy(DECONZ_WEB_REQUEST) + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ) as forward_entry_setup: + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert gateway.bridgeid == BRIDGEID + assert gateway.master is True + assert gateway.option_allow_clip_sensor is False + assert gateway.option_allow_deconz_groups is True + + assert len(gateway.deconz_ids) == 0 + assert len(hass.states.async_all()) == 0 + + entry = gateway.config_entry + assert forward_entry_setup.mock_calls[0][1] == (entry, "binary_sensor") + assert forward_entry_setup.mock_calls[1][1] == (entry, "climate") + assert forward_entry_setup.mock_calls[2][1] == (entry, "cover") + assert forward_entry_setup.mock_calls[3][1] == (entry, "light") + assert forward_entry_setup.mock_calls[4][1] == (entry, "scene") + assert forward_entry_setup.mock_calls[5][1] == (entry, "sensor") + assert forward_entry_setup.mock_calls[6][1] == (entry, "switch") + + +async def test_gateway_retry(hass): """Retry setup.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - - deconz_gateway = gateway.DeconzGateway(hass, entry) + data = deepcopy(DECONZ_WEB_REQUEST) + with patch( + "homeassistant.components.deconz.gateway.get_gateway", + side_effect=deconz.errors.CannotConnect, + ), pytest.raises(ConfigEntryNotReady): + await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) - with patch.object(gateway, "get_gateway", side_effect=Exception): - result = await deconz_gateway.async_setup() - assert not result +async def test_gateway_setup_fails(hass): + """Retry setup.""" + data = deepcopy(DECONZ_WEB_REQUEST) + with patch( + "homeassistant.components.deconz.gateway.get_gateway", side_effect=Exception + ): + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert gateway is None -async def test_connection_status(hass): +async def test_connection_status_signalling(hass): """Make sure that connection status triggers a dispatcher send.""" - entry = Mock() - entry.data = ENTRY_CONFIG - - deconz_gateway = gateway.DeconzGateway(hass, entry) - with patch.object(gateway, "async_dispatcher_send") as mock_dispatch_send: - deconz_gateway.async_connection_status_callback(True) - - await hass.async_block_till_done() - assert len(mock_dispatch_send.mock_calls) == 1 - assert len(mock_dispatch_send.mock_calls[0]) == 3 - - -async def test_add_device(hass): - """Successful retry setup.""" - entry = Mock() - entry.data = ENTRY_CONFIG - - deconz_gateway = gateway.DeconzGateway(hass, entry) - with patch.object(gateway, "async_dispatcher_send") as mock_dispatch_send: - deconz_gateway.async_add_device_callback("sensor", Mock()) - - await hass.async_block_till_done() - assert len(mock_dispatch_send.mock_calls) == 1 - assert len(mock_dispatch_send.mock_calls[0]) == 3 - - -async def test_shutdown(): - """Successful shutdown.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - - deconz_gateway = gateway.DeconzGateway(hass, entry) - deconz_gateway.api = Mock() - deconz_gateway.shutdown(None) + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) - assert len(deconz_gateway.api.close.mock_calls) == 1 + event_call = Mock() + unsub = async_dispatcher_connect(hass, gateway.signal_reachable, event_call) + gateway.async_connection_status_callback(False) + await hass.async_block_till_done() -async def test_reset_after_successful_setup(): - """Verify that reset works on a setup component.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - api = Mock() - api.async_add_remote.return_value = Mock() - api.sensors = {} + assert gateway.available is False + assert len(event_call.mock_calls) == 1 - deconz_gateway = gateway.DeconzGateway(hass, entry) + unsub() - with patch.object( - gateway, "get_gateway", return_value=mock_coro(api) - ), patch.object(gateway, "async_dispatcher_connect", return_value=Mock()): - assert await deconz_gateway.async_setup() is True - listener = Mock() - deconz_gateway.listeners = [listener] - event = Mock() - event.async_will_remove_from_hass = Mock() - deconz_gateway.events = [event] - deconz_gateway.deconz_ids = {"key": "value"} +async def test_update_address(hass): + """Make sure that connection status triggers a dispatcher send.""" + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert gateway.api.host == "1.2.3.4" + + await hass.config_entries.flow.async_init( + deconz.config_flow.DOMAIN, + data={ + deconz.config_flow.CONF_HOST: "2.3.4.5", + deconz.config_flow.CONF_PORT: 80, + ssdp.ATTR_SERIAL: BRIDGEID, + ssdp.ATTR_MANUFACTURERURL: deconz.config_flow.DECONZ_MANUFACTURERURL, + deconz.config_flow.ATTR_UUID: "uuid:1234", + }, + context={"source": "ssdp"}, + ) + await hass.async_block_till_done() - hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) - assert await deconz_gateway.async_reset() is True + assert gateway.api.host == "2.3.4.5" - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 7 - assert len(listener.mock_calls) == 1 - assert len(deconz_gateway.listeners) == 0 +async def test_reset_after_successful_setup(hass): + """Make sure that connection status triggers a dispatcher send.""" + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) - assert len(event.async_will_remove_from_hass.mock_calls) == 1 - assert len(deconz_gateway.events) == 0 + result = await gateway.async_reset() + await hass.async_block_till_done() - assert len(deconz_gateway.deconz_ids) == 0 + assert result is True async def test_get_gateway(hass): """Successful call.""" - with patch( - "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(True) - ): - assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) + with patch("pydeconz.DeconzSession.async_load_parameters", return_value=True): + assert await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) async def test_get_gateway_fails_unauthorized(hass): @@ -189,8 +180,11 @@ async def test_get_gateway_fails_unauthorized(hass): with patch( "pydeconz.DeconzSession.async_load_parameters", side_effect=pydeconz.errors.Unauthorized, - ), pytest.raises(errors.AuthenticationRequired): - assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False + ), pytest.raises(deconz.errors.AuthenticationRequired): + assert ( + await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) + is False + ) async def test_get_gateway_fails_cannot_connect(hass): @@ -198,5 +192,8 @@ async def test_get_gateway_fails_cannot_connect(hass): with patch( "pydeconz.DeconzSession.async_load_parameters", side_effect=pydeconz.errors.RequestError, - ), pytest.raises(errors.CannotConnect): - assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False + ), pytest.raises(deconz.errors.CannotConnect): + assert ( + await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) + is False + ) diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 50a5b2adacabcb..14dc5cc8eac1c7 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -3,12 +3,13 @@ from asynctest import patch -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component import homeassistant.components.light as light +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration + GROUPS = { "1": { "id": "Light group id", @@ -61,50 +62,6 @@ }, } -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - - -async def setup_deconz_integration(hass, config, options, get_state_response): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response - ), patch("pydeconz.DeconzSession.start", return_value=True): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" @@ -242,6 +199,10 @@ async def test_lights_and_groups(hass): await hass.async_block_till_done() set_callback.assert_called_with("/lights/1/state", {"alert": "lselect"}) + await gateway.async_reset() + + assert len(hass.states.async_all()) == 2 + async def test_disable_light_groups(hass): """Test successful creation of sensor entities.""" diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 074e943548de40..dcc8ba500c32f7 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -1,67 +1,28 @@ """deCONZ scene platform tests.""" -from unittest.mock import Mock, patch +from copy import deepcopy + +from asynctest import patch -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component import homeassistant.components.scene as scene -from tests.common import mock_coro - +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration -GROUP = { +GROUPS = { "1": { - "id": "Group 1 id", - "name": "Group 1 name", - "state": {}, + "id": "Light group id", + "name": "Light group", + "type": "LightGroup", + "state": {"all_on": False, "any_on": True}, "action": {}, - "scenes": [{"id": "1", "name": "Scene 1"}], + "scenes": [{"id": "1", "name": "Scene"}], "lights": [], } } -ENTRY_CONFIG = { - deconz.const.CONF_ALLOW_CLIP_SENSOR: True, - deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: "0123456789", - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - - -async def setup_gateway(hass, data): - """Load the deCONZ scene platform.""" - from pydeconz import DeconzSession - - loop = Mock() - session = Mock() - - config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - - await hass.config_entries.async_forward_entry_setup(config_entry, "scene") - # To flush out the service call to update the group - await hass.async_block_till_done() - return gateway - - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" assert ( @@ -75,26 +36,38 @@ async def test_platform_manually_configured(hass): async def test_no_scenes(hass): """Test that scenes can be loaded without scenes being available.""" - gateway = await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_scenes(hass): """Test that scenes works.""" - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - gateway = await setup_gateway(hass, {"groups": GROUP}) - assert "scene.group_1_name_scene_1" in gateway.deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + data["groups"] = deepcopy(GROUPS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + + assert "scene.light_group_scene" in gateway.deconz_ids assert len(hass.states.async_all()) == 1 - await hass.services.async_call( - "scene", "turn_on", {"entity_id": "scene.group_1_name_scene_1"}, blocking=True - ) + light_group_scene = hass.states.get("scene.light_group_scene") + assert light_group_scene + group_scene = gateway.api.groups["1"].scenes["1"] -async def test_unload_scene(hass): - """Test that it works to unload scene entities.""" - gateway = await setup_gateway(hass, {"groups": GROUP}) + with patch.object( + group_scene, "_async_set_state_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + "scene", "turn_on", {"entity_id": "scene.light_group_scene"}, blocking=True + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/groups/1/scenes/1/recall", {}) await gateway.async_reset() diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 947c42e6949834..928e527dd0706e 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -1,14 +1,12 @@ """deCONZ sensor platform tests.""" from copy import deepcopy -from asynctest import patch - -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component import homeassistant.components.sensor as sensor +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration SENSORS = { "1": { @@ -77,50 +75,6 @@ }, } -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - - -async def setup_deconz_integration(hass, config, options, get_state_response): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response - ), patch("pydeconz.DeconzSession.start", return_value=True): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" @@ -199,6 +153,10 @@ async def test_sensors(hass): switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") assert switch_2_battery_level.state == "75" + await gateway.async_reset() + + assert len(hass.states.async_all()) == 0 + async def test_allow_clip_sensors(hass): """Test that CLIP sensors can be allowed.""" diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 63934871fcbf39..533d85eef7cbe3 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -1,30 +1,19 @@ """deCONZ service tests.""" +from copy import deepcopy + from asynctest import Mock, patch import pytest import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import deconz -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} +from .test_gateway import ( + BRIDGEID, + ENTRY_CONFIG, + DECONZ_WEB_REQUEST, + setup_deconz_integration, +) GROUP = { "1": { @@ -60,31 +49,6 @@ } -async def setup_deconz_integration(hass, options): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=ENTRY_CONFIG, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=DECONZ_WEB_REQUEST - ): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][BRIDGEID] - - async def test_service_setup(hass): """Verify service setup works.""" assert deconz.services.DECONZ_SERVICES not in hass.data @@ -129,7 +93,10 @@ async def test_service_unload_not_registered(hass): async def test_configure_service_with_field(hass): """Test that service invokes pydeconz with the correct path and data.""" - await setup_deconz_integration(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) data = { deconz.services.SERVICE_FIELD: "/light/2", @@ -149,7 +116,10 @@ async def test_configure_service_with_field(hass): async def test_configure_service_with_entity(hass): """Test that service invokes pydeconz with the correct path and data.""" - gateway = await setup_deconz_integration(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) gateway.deconz_ids["light.test"] = "/light/1" data = { @@ -169,7 +139,10 @@ async def test_configure_service_with_entity(hass): async def test_configure_service_with_entity_and_field(hass): """Test that service invokes pydeconz with the correct path and data.""" - gateway = await setup_deconz_integration(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) gateway.deconz_ids["light.test"] = "/light/1" data = { @@ -192,7 +165,10 @@ async def test_configure_service_with_entity_and_field(hass): async def test_configure_service_with_faulty_field(hass): """Test that service invokes pydeconz with the correct path and data.""" - await setup_deconz_integration(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) data = {deconz.services.SERVICE_FIELD: "light/2", deconz.services.SERVICE_DATA: {}} @@ -205,7 +181,10 @@ async def test_configure_service_with_faulty_field(hass): async def test_configure_service_with_faulty_entity(hass): """Test that service invokes pydeconz with the correct path and data.""" - await setup_deconz_integration(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) data = { deconz.services.SERVICE_ENTITY: "light.nonexisting", @@ -224,7 +203,10 @@ async def test_configure_service_with_faulty_entity(hass): async def test_service_refresh_devices(hass): """Test that service can refresh devices.""" - gateway = await setup_deconz_integration(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) data = {deconz.CONF_BRIDGEID: BRIDGEID} diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index c574ed8911e063..262bd7001f5846 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -3,13 +3,13 @@ from asynctest import patch -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component - import homeassistant.components.switch as switch +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration + SWITCHES = { "1": { "id": "On off switch id", @@ -41,50 +41,6 @@ }, } -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - - -async def setup_deconz_integration(hass, config, options, get_state_response): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response - ), patch("pydeconz.DeconzSession.start", return_value=True): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" @@ -189,3 +145,7 @@ async def test_switches(hass): ) await hass.async_block_till_done() set_callback.assert_called_with("/lights/3/state", {"alert": "none"}) + + await gateway.async_reset() + + assert len(hass.states.async_all()) == 2 From 7a1bfa7a1fab30a10b72bfd5fa8500288ecedf5c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Sep 2019 15:23:29 -0700 Subject: [PATCH 080/296] Updated frontend to 20190919.0 --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 978127c6342028..18c19a9012a7da 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190918.1" + "home-assistant-frontend==20190919.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 746485f2ece2da..900bfddda2ec10 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190918.1 +home-assistant-frontend==20190919.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 437aa60cf96d15..29897b562270e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190918.1 +home-assistant-frontend==20190919.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f5a1bef6e469a..122ff317c0e511 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190918.1 +home-assistant-frontend==20190919.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 20e61fb41b9eb6d7b79862ee39aa202dc344dc7f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 20 Sep 2019 00:32:11 +0000 Subject: [PATCH 081/296] [ci skip] Translation update --- .../components/izone/.translations/it.json | 15 +++++++++ .../components/plex/.translations/en.json | 33 +++++++++++++++++++ .../components/plex/.translations/it.json | 33 +++++++++++++++++++ .../components/switch/.translations/it.json | 2 ++ .../components/switch/.translations/pl.json | 2 ++ .../components/switch/.translations/sl.json | 2 ++ .../switch/.translations/zh-Hant.json | 2 ++ .../components/withings/.translations/it.json | 3 ++ .../components/withings/.translations/pl.json | 3 ++ .../components/withings/.translations/sl.json | 3 ++ .../withings/.translations/zh-Hant.json | 3 ++ 11 files changed, 101 insertions(+) create mode 100644 homeassistant/components/izone/.translations/it.json create mode 100644 homeassistant/components/plex/.translations/en.json create mode 100644 homeassistant/components/plex/.translations/it.json diff --git a/homeassistant/components/izone/.translations/it.json b/homeassistant/components/izone/.translations/it.json new file mode 100644 index 00000000000000..5498624a061ed4 --- /dev/null +++ b/homeassistant/components/izone/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo iZone trovato in rete.", + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di iZone." + }, + "step": { + "confirm": { + "description": "Vuoi configurare iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json new file mode 100644 index 00000000000000..2ada5e810ecbaa --- /dev/null +++ b/homeassistant/components/plex/.translations/en.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "All linked servers already configured", + "already_configured": "This Plex server is already configured", + "already_in_progress": "Plex is being configured", + "invalid_import": "Imported configuration is invalid", + "unknown": "Failed for unknown reason" + }, + "error": { + "faulty_credentials": "Authorization failed", + "no_servers": "No servers linked to account", + "not_found": "Plex server not found" + }, + "step": { + "select_server": { + "data": { + "server": "Server" + }, + "description": "Multiple servers available, select one:", + "title": "Select Plex server" + }, + "user": { + "data": { + "token": "Plex token" + }, + "description": "Enter a Plex token for automatic setup.", + "title": "Connect Plex server" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json new file mode 100644 index 00000000000000..2e77b4ba9768b5 --- /dev/null +++ b/homeassistant/components/plex/.translations/it.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "Tutti i server collegati sono gi\u00e0 configurati", + "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", + "already_in_progress": "Plex \u00e8 in fase di configurazione", + "invalid_import": "La configurazione importata non \u00e8 valida", + "unknown": "Non riuscito per motivo sconosciuto" + }, + "error": { + "faulty_credentials": "Autorizzazione non riuscita", + "no_servers": "Nessun server collegato all'account", + "not_found": "Server Plex non trovato" + }, + "step": { + "select_server": { + "data": { + "server": "Server" + }, + "description": "Sono disponibili pi\u00f9 server, selezionarne uno:", + "title": "Selezionare il server Plex" + }, + "user": { + "data": { + "token": "Token Plex" + }, + "description": "Immettere un token Plex per la configurazione automatica.", + "title": "Collegare il server Plex" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/it.json b/homeassistant/components/switch/.translations/it.json index 254c09380c1d1d..ec742e4113bd7c 100644 --- a/homeassistant/components/switch/.translations/it.json +++ b/homeassistant/components/switch/.translations/it.json @@ -6,6 +6,8 @@ "turn_on": "Attivare {entity_name}" }, "condition_type": { + "is_off": "{entity_name} \u00e8 disattivato", + "is_on": "{entity_name} \u00e8 attivo", "turn_off": "{entity_name} disattivato", "turn_on": "{entity_name} attivato" }, diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index c63799bf783923..921048286b6a82 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -6,6 +6,8 @@ "turn_on": "W\u0142\u0105cz {entity_name}" }, "condition_type": { + "is_off": "(entity_name} jest wy\u0142\u0105czony.", + "is_on": "{entity_name} jest w\u0142\u0105czony", "turn_off": "{entity_name} wy\u0142\u0105czone", "turn_on": "{entity_name} w\u0142\u0105czone" }, diff --git a/homeassistant/components/switch/.translations/sl.json b/homeassistant/components/switch/.translations/sl.json index 89423e071fdc8b..f1b851b05b6f4b 100644 --- a/homeassistant/components/switch/.translations/sl.json +++ b/homeassistant/components/switch/.translations/sl.json @@ -6,6 +6,8 @@ "turn_on": "Vklopite {entity_name}" }, "condition_type": { + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen", "turn_off": "{entity_name} izklopljen", "turn_on": "{entity_name} vklopljen" }, diff --git a/homeassistant/components/switch/.translations/zh-Hant.json b/homeassistant/components/switch/.translations/zh-Hant.json index c1a67897b16c84..517d48354dcc2d 100644 --- a/homeassistant/components/switch/.translations/zh-Hant.json +++ b/homeassistant/components/switch/.translations/zh-Hant.json @@ -6,6 +6,8 @@ "turn_on": "\u958b\u555f {entity_name}" }, "condition_type": { + "is_off": "{entity_name} \u5df2\u95dc\u9589", + "is_on": "{entity_name} \u5df2\u958b\u555f", "turn_off": "{entity_name} \u5df2\u95dc\u9589", "turn_on": "{entity_name} \u5df2\u958b\u555f" }, diff --git a/homeassistant/components/withings/.translations/it.json b/homeassistant/components/withings/.translations/it.json index 5bf342836ce61a..51276869ec605c 100644 --- a/homeassistant/components/withings/.translations/it.json +++ b/homeassistant/components/withings/.translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "\u00c8 necessario configurare Withings prima di potersi autenticare con esso. Si prega di leggere la documentazione." + }, "create_entry": { "default": "Autenticazione completata con Withings per il profilo selezionato." }, diff --git a/homeassistant/components/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json index 1643ecb148012b..3c345a1a788bd6 100644 --- a/homeassistant/components/withings/.translations/pl.json +++ b/homeassistant/components/withings/.translations/pl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Musisz skonfigurowa\u0107 Withings, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Przeczytaj prosz\u0119 dokumentacj\u0119." + }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Withings dla wybranego profilu" }, diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json index d0fcb6a5276b61..71934516ea7f1b 100644 --- a/homeassistant/components/withings/.translations/sl.json +++ b/homeassistant/components/withings/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo." + }, "create_entry": { "default": "Uspe\u0161no overjen z Withings za izbrani profil." }, diff --git a/homeassistant/components/withings/.translations/zh-Hant.json b/homeassistant/components/withings/.translations/zh-Hant.json index 30a77102d04a31..9e408eb0d5c19e 100644 --- a/homeassistant/components/withings/.translations/zh-Hant.json +++ b/homeassistant/components/withings/.translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Withings \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002\u8acb\u53c3\u95b1\u6587\u4ef6\u3002" + }, "create_entry": { "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u88dd\u7f6e\u3002" }, From 9e44d1af1991dd3822b4664c94c1fe7e5410fd7e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 20 Sep 2019 15:55:43 +0200 Subject: [PATCH 082/296] Revert "Add transport data from maps.yandex.ru api (#26252)" (#26762) This reverts commit 9e2cd5116acee97cc09453e564e419371e8e3e9c. --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/yandex_transport/__init__.py | 1 - .../components/yandex_transport/manifest.json | 12 - .../components/yandex_transport/sensor.py | 128 - requirements_all.txt | 3 - tests/components/yandex_transport/__init__.py | 1 - .../test_yandex_transport_sensor.py | 88 - tests/fixtures/yandex_transport_reply.json | 2106 ----------------- 9 files changed, 2341 deletions(-) delete mode 100644 homeassistant/components/yandex_transport/__init__.py delete mode 100644 homeassistant/components/yandex_transport/manifest.json delete mode 100644 homeassistant/components/yandex_transport/sensor.py delete mode 100644 tests/components/yandex_transport/__init__.py delete mode 100644 tests/components/yandex_transport/test_yandex_transport_sensor.py delete mode 100644 tests/fixtures/yandex_transport_reply.json diff --git a/.coveragerc b/.coveragerc index a29586c7b6e1cd..5d5b0f6c81b09b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -747,7 +747,6 @@ omit = homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yamaha/media_player.py homeassistant/components/yamaha_musiccast/media_player.py - homeassistant/components/yandex_transport/* homeassistant/components/yeelight/* homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py diff --git a/CODEOWNERS b/CODEOWNERS index 9bcd475d5d4788..9766f011889fe9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -318,7 +318,6 @@ homeassistant/components/xiaomi_miio/* @rytilahti @syssi homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yamaha_musiccast/* @jalmeroth -homeassistant/components/yandex_transport/* @rishatik92 homeassistant/components/yeelight/* @rytilahti @zewelor homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yessssms/* @flowolf diff --git a/homeassistant/components/yandex_transport/__init__.py b/homeassistant/components/yandex_transport/__init__.py deleted file mode 100644 index d007b2d3df8581..00000000000000 --- a/homeassistant/components/yandex_transport/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Service for obtaining information about closer bus from Transport Yandex Service.""" diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json deleted file mode 100644 index 54837b2eb0914d..00000000000000 --- a/homeassistant/components/yandex_transport/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "yandex_transport", - "name": "Yandex Transport", - "documentation": "https://www.home-assistant.io/components/yandex_transport", - "requirements": [ - "ya_ma==0.3.4" - ], - "dependencies": [], - "codeowners": [ - "@rishatik92" - ] -} diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py deleted file mode 100644 index 340291807ead98..00000000000000 --- a/homeassistant/components/yandex_transport/sensor.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- -"""Service for obtaining information about closer bus from Transport Yandex Service.""" - -import logging -from datetime import timedelta - -import voluptuous as vol -from ya_ma import YandexMapsRequester - -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) - -STOP_NAME = "stop_name" -USER_AGENT = "Home Assistant" -ATTRIBUTION = "Data provided by maps.yandex.ru" - -CONF_STOP_ID = "stop_id" -CONF_ROUTE = "routes" - -DEFAULT_NAME = "Yandex Transport" -ICON = "mdi:bus" - -SCAN_INTERVAL = timedelta(minutes=1) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_STOP_ID): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_ROUTE, default=[]): vol.All(cv.ensure_list, [cv.string]), - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Yandex transport sensor.""" - stop_id = config[CONF_STOP_ID] - name = config[CONF_NAME] - routes = config[CONF_ROUTE] - - data = YandexMapsRequester(user_agent=USER_AGENT) - add_entities([DiscoverMoscowYandexTransport(data, stop_id, routes, name)], True) - - -class DiscoverMoscowYandexTransport(Entity): - """Implementation of yandex_transport sensor.""" - - def __init__(self, requester, stop_id, routes, name): - """Initialize sensor.""" - self.requester = requester - self._stop_id = stop_id - self._routes = [] - self._routes = routes - self._state = None - self._name = name - self._attrs = None - - def update(self): - """Get the latest data from maps.yandex.ru and update the states.""" - attrs = {} - closer_time = None - try: - yandex_reply = self.requester.get_stop_info(self._stop_id) - data = yandex_reply["data"] - stop_metadata = data["properties"]["StopMetaData"] - except KeyError as key_error: - _LOGGER.warning( - "Exception KeyError was captured, missing key is %s. Yandex returned: %s", - key_error, - yandex_reply, - ) - self.requester.set_new_session() - data = self.requester.get_stop_info(self._stop_id)["data"] - stop_metadata = data["properties"]["StopMetaData"] - stop_name = data["properties"]["name"] - transport_list = stop_metadata["Transport"] - for transport in transport_list: - route = transport["name"] - if self._routes and route not in self._routes: - # skip unnecessary route info - continue - if "Events" in transport["BriefSchedule"]: - for event in transport["BriefSchedule"]["Events"]: - if "Estimated" in event: - posix_time_next = int(event["Estimated"]["value"]) - if closer_time is None or closer_time > posix_time_next: - closer_time = posix_time_next - if route not in attrs: - attrs[route] = [] - attrs[route].append(event["Estimated"]["text"]) - attrs[STOP_NAME] = stop_name - attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - if closer_time is None: - self._state = None - else: - self._state = dt_util.utc_from_timestamp(closer_time).isoformat( - timespec="seconds" - ) - self._attrs = attrs - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attrs - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 29897b562270e7..ac49c415398022 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1994,9 +1994,6 @@ xmltodict==0.12.0 # homeassistant.components.xs1 xs1-api-client==2.3.5 -# homeassistant.components.yandex_transport -ya_ma==0.3.4 - # homeassistant.components.yweather yahooweather==0.10 diff --git a/tests/components/yandex_transport/__init__.py b/tests/components/yandex_transport/__init__.py deleted file mode 100644 index fe6b0db52d3e05..00000000000000 --- a/tests/components/yandex_transport/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the yandex transport platform.""" diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py deleted file mode 100644 index 50d945e7fae371..00000000000000 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Tests for the yandex transport platform.""" - -import json -import pytest - -import homeassistant.components.sensor as sensor -import homeassistant.util.dt as dt_util -from homeassistant.const import CONF_NAME -from tests.common import ( - assert_setup_component, - async_setup_component, - MockDependency, - load_fixture, -) - -REPLY = json.loads(load_fixture("yandex_transport_reply.json")) - - -@pytest.fixture -def mock_requester(): - """Create a mock ya_ma module and YandexMapsRequester.""" - with MockDependency("ya_ma") as ya_ma: - instance = ya_ma.YandexMapsRequester.return_value - instance.get_stop_info.return_value = REPLY - yield instance - - -STOP_ID = 9639579 -ROUTES = ["194", "т36", "т47", "м10"] -NAME = "test_name" -TEST_CONFIG = { - "sensor": { - "platform": "yandex_transport", - "stop_id": 9639579, - "routes": ROUTES, - "name": NAME, - } -} - -FILTERED_ATTRS = { - "т36": ["21:43", "21:47", "22:02"], - "т47": ["21:40", "22:01"], - "м10": ["21:48", "22:00"], - "stop_name": "7-й автобусный парк", - "attribution": "Data provided by maps.yandex.ru", -} - -RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds") - - -async def assert_setup_sensor(hass, config, count=1): - """Set up the sensor and assert it's been created.""" - with assert_setup_component(count): - assert await async_setup_component(hass, sensor.DOMAIN, config) - - -async def test_setup_platform_valid_config(hass, mock_requester): - """Test that sensor is set up properly with valid config.""" - await assert_setup_sensor(hass, TEST_CONFIG) - - -async def test_setup_platform_invalid_config(hass, mock_requester): - """Check an invalid configuration.""" - await assert_setup_sensor( - hass, {"sensor": {"platform": "yandex_transport", "stopid": 1234}}, count=0 - ) - - -async def test_name(hass, mock_requester): - """Return the name if set in the configuration.""" - await assert_setup_sensor(hass, TEST_CONFIG) - state = hass.states.get("sensor.test_name") - assert state.name == TEST_CONFIG["sensor"][CONF_NAME] - - -async def test_state(hass, mock_requester): - """Return the contents of _state.""" - await assert_setup_sensor(hass, TEST_CONFIG) - state = hass.states.get("sensor.test_name") - assert state.state == RESULT_STATE - - -async def test_filtered_attributes(hass, mock_requester): - """Return the contents of attributes.""" - await assert_setup_sensor(hass, TEST_CONFIG) - state = hass.states.get("sensor.test_name") - state_attrs = {key: state.attributes[key] for key in FILTERED_ATTRS} - assert state_attrs == FILTERED_ATTRS diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_reply.json deleted file mode 100644 index c5e4857297aa93..00000000000000 --- a/tests/fixtures/yandex_transport_reply.json +++ /dev/null @@ -1,2106 +0,0 @@ -{ - "data": { - "geometries": [ - { - "type": "Point", - "coordinates": [ - 37.565280044, - 55.851959656 - ] - } - ], - "geometry": { - "type": "Point", - "coordinates": [ - 37.565280044, - 55.851959656 - ] - }, - "properties": { - "name": "7-й автобусный парк", - "description": "7-й автобусный парк", - "currentTime": "Mon Sep 16 2019 21:40:40 GMT+0300 (Moscow Standard Time)", - "StopMetaData": { - "id": "stop__9639579", - "name": "7-й автобусный парк", - "type": "urban", - "region": { - "id": 213, - "type": 6, - "parent_id": 1, - "capital_id": 0, - "geo_parent_id": 0, - "city_id": 213, - "name": "moscow", - "native_name": "", - "iso_name": "RU MOW", - "is_main": true, - "en_name": "Moscow", - "short_en_name": "MSK", - "phone_code": "495 499", - "phone_code_old": "095", - "zip_code": "", - "population": 12506468, - "synonyms": "Moskau, Moskva", - "latitude": 55.753215, - "longitude": 37.622504, - "latitude_size": 0.878654, - "longitude_size": 1.164423, - "zoom": 10, - "tzname": "Europe/Moscow", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "weather", - "afisha", - "maps", - "tv", - "ad", - "etrain", - "subway", - "delivery", - "route" - ], - "ename": "moscow", - "bounds": [ - [ - 37.0402925, - 55.31141404514547 - ], - [ - 38.2047155, - 56.190068045145466 - ] - ], - "names": { - "ablative": "", - "accusative": "Москву", - "dative": "Москве", - "directional": "", - "genitive": "Москвы", - "instrumental": "Москвой", - "locative": "", - "nominative": "Москва", - "preposition": "в", - "prepositional": "Москве" - }, - "parent": { - "id": 1, - "type": 5, - "parent_id": 3, - "capital_id": 213, - "geo_parent_id": 0, - "city_id": 213, - "name": "moscow-and-moscow-oblast", - "native_name": "", - "iso_name": "RU-MOS", - "is_main": true, - "en_name": "Moscow and Moscow Oblast", - "short_en_name": "RU-MOS", - "phone_code": "495 496 498 499", - "phone_code_old": "", - "zip_code": "", - "population": 7503385, - "synonyms": "Московская область, Подмосковье, Podmoskovye", - "latitude": 55.815792, - "longitude": 37.380031, - "latitude_size": 2.705659, - "longitude_size": 5.060749, - "zoom": 8, - "tzname": "Europe/Moscow", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [ - 213, - 10716, - 10747, - 10758, - 20728, - 10740, - 10738, - 20523, - 10735, - 10734, - 10743, - 21622 - ], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "ad" - ], - "ename": "moscow-and-moscow-oblast", - "bounds": [ - [ - 34.8496565, - 54.439456064325434 - ], - [ - 39.9104055, - 57.14511506432543 - ] - ], - "names": { - "ablative": "", - "accusative": "Москву и Московскую область", - "dative": "Москве и Московской области", - "directional": "", - "genitive": "Москвы и Московской области", - "instrumental": "Москвой и Московской областью", - "locative": "", - "nominative": "Москва и Московская область", - "preposition": "в", - "prepositional": "Москве и Московской области" - }, - "parent": { - "id": 225, - "type": 3, - "parent_id": 10001, - "capital_id": 213, - "geo_parent_id": 0, - "city_id": 213, - "name": "russia", - "native_name": "", - "iso_name": "RU", - "is_main": false, - "en_name": "Russia", - "short_en_name": "RU", - "phone_code": "7", - "phone_code_old": "", - "zip_code": "", - "population": 146880432, - "synonyms": "Russian Federation,Российская Федерация", - "latitude": 61.698653, - "longitude": 99.505405, - "latitude_size": 40.700127, - "longitude_size": 171.643239, - "zoom": 3, - "tzname": "", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [ - 213, - 2, - 65, - 54, - 47, - 43, - 66, - 51, - 56, - 172, - 39, - 62 - ], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "ad" - ], - "ename": "russia", - "bounds": [ - [ - 13.683785499999999, - 35.290400699917846 - ], - [ - -174.6729755, - 75.99052769991785 - ] - ], - "names": { - "ablative": "", - "accusative": "Россию", - "dative": "России", - "directional": "", - "genitive": "России", - "instrumental": "Россией", - "locative": "", - "nominative": "Россия", - "preposition": "в", - "prepositional": "России" - } - } - } - }, - "Transport": [ - { - "lineId": "2036925416", - "name": "194", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036927196", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9648742", - "name": "Коровино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659860", - "tzOffset": 10800, - "text": "21:51" - } - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661840", - "tzOffset": 10800, - "text": "22:24" - } - } - ], - "departureTime": "21:51" - } - } - ], - "threadId": "2036927196", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9648742", - "name": "Коровино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659860", - "tzOffset": 10800, - "text": "21:51" - } - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661840", - "tzOffset": 10800, - "text": "22:24" - } - } - ], - "departureTime": "21:51" - } - }, - { - "lineId": "213_114_bus_mosgortrans", - "name": "114", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_114_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568603405", - "tzOffset": 10800, - "text": "6:10" - }, - "end": { - "value": "1568672165", - "tzOffset": 10800, - "text": "1:16" - } - } - } - } - ], - "threadId": "213B_114_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568603405", - "tzOffset": 10800, - "text": "6:10" - }, - "end": { - "value": "1568672165", - "tzOffset": 10800, - "text": "1:16" - } - } - } - }, - { - "lineId": "213_154_bus_mosgortrans", - "name": "154", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_154_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642548", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659260", - "tzOffset": 10800, - "text": "21:41" - }, - "Estimated": { - "value": "1568659252", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1054764%5F191500" - }, - { - "Scheduled": { - "value": "1568660580", - "tzOffset": 10800, - "text": "22:03" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:41" - } - } - ], - "threadId": "213B_154_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642548", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659260", - "tzOffset": 10800, - "text": "21:41" - }, - "Estimated": { - "value": "1568659252", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1054764%5F191500" - }, - { - "Scheduled": { - "value": "1568660580", - "tzOffset": 10800, - "text": "22:03" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:41" - } - }, - { - "lineId": "213_179_bus_mosgortrans", - "name": "179", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_179_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568659351", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|59832%5F31359" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661660", - "tzOffset": 10800, - "text": "22:21" - } - } - ], - "departureTime": "21:52" - } - } - ], - "threadId": "213B_179_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568659351", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|59832%5F31359" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661660", - "tzOffset": 10800, - "text": "22:21" - } - } - ], - "departureTime": "21:52" - } - }, - { - "lineId": "213_191m_minibus_default", - "name": "591", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_191m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660525", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|38278%5F9345312" - } - ], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568602033", - "tzOffset": 10800, - "text": "5:47" - }, - "end": { - "value": "1568672233", - "tzOffset": 10800, - "text": "1:17" - } - } - } - } - ], - "threadId": "213A_191m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660525", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|38278%5F9345312" - } - ], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568602033", - "tzOffset": 10800, - "text": "5:47" - }, - "end": { - "value": "1568672233", - "tzOffset": 10800, - "text": "1:17" - } - } - } - }, - { - "lineId": "213_206m_minibus_default", - "name": "206к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_206m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568601239", - "tzOffset": 10800, - "text": "5:33" - }, - "end": { - "value": "1568671439", - "tzOffset": 10800, - "text": "1:03" - } - } - } - } - ], - "threadId": "213A_206m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568601239", - "tzOffset": 10800, - "text": "5:33" - }, - "end": { - "value": "1568671439", - "tzOffset": 10800, - "text": "1:03" - } - } - } - }, - { - "lineId": "213_215_bus_mosgortrans", - "name": "215", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_215_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "27 мин", - "value": 1620, - "begin": { - "value": "1568601276", - "tzOffset": 10800, - "text": "5:34" - }, - "end": { - "value": "1568671476", - "tzOffset": 10800, - "text": "1:04" - } - } - } - } - ], - "threadId": "213B_215_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "27 мин", - "value": 1620, - "begin": { - "value": "1568601276", - "tzOffset": 10800, - "text": "5:34" - }, - "end": { - "value": "1568671476", - "tzOffset": 10800, - "text": "1:04" - } - } - } - }, - { - "lineId": "213_282_bus_mosgortrans", - "name": "282", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_282_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9641102", - "name": "Улица Корнейчука" - }, - { - "id": "2532226085", - "name": "Метро Войковская" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659888", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|34874%5F9345408" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568602180", - "tzOffset": 10800, - "text": "5:49" - }, - "end": { - "value": "1568673460", - "tzOffset": 10800, - "text": "1:37" - } - } - } - } - ], - "threadId": "213A_282_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9641102", - "name": "Улица Корнейчука" - }, - { - "id": "2532226085", - "name": "Метро Войковская" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659888", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|34874%5F9345408" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568602180", - "tzOffset": 10800, - "text": "5:49" - }, - "end": { - "value": "1568673460", - "tzOffset": 10800, - "text": "1:37" - } - } - } - }, - { - "lineId": "213_294m_minibus_default", - "name": "994", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_294m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9649459", - "name": "Метро Алтуфьево" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "30 мин", - "value": 1800, - "begin": { - "value": "1568601527", - "tzOffset": 10800, - "text": "5:38" - }, - "end": { - "value": "1568671727", - "tzOffset": 10800, - "text": "1:08" - } - } - } - } - ], - "threadId": "213A_294m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9649459", - "name": "Метро Алтуфьево" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "30 мин", - "value": 1800, - "begin": { - "value": "1568601527", - "tzOffset": 10800, - "text": "5:38" - }, - "end": { - "value": "1568671727", - "tzOffset": 10800, - "text": "1:08" - } - } - } - }, - { - "lineId": "213_36_trolleybus_mosgortrans", - "name": "т36", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_36_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642550", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9640641", - "name": "Дмитровское шоссе, 155" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - }, - "Estimated": { - "value": "1568659426", - "tzOffset": 10800, - "text": "21:43" - }, - "vehicleId": "codd%5Fnew|1084829%5F430260" - }, - { - "Scheduled": { - "value": "1568660520", - "tzOffset": 10800, - "text": "22:02" - }, - "Estimated": { - "value": "1568659656", - "tzOffset": 10800, - "text": "21:47" - }, - "vehicleId": "codd%5Fnew|1117016%5F430280" - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - }, - "Estimated": { - "value": "1568660538", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|1054576%5F430226" - } - ], - "departureTime": "21:48" - } - } - ], - "threadId": "213A_36_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642550", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9640641", - "name": "Дмитровское шоссе, 155" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - }, - "Estimated": { - "value": "1568659426", - "tzOffset": 10800, - "text": "21:43" - }, - "vehicleId": "codd%5Fnew|1084829%5F430260" - }, - { - "Scheduled": { - "value": "1568660520", - "tzOffset": 10800, - "text": "22:02" - }, - "Estimated": { - "value": "1568659656", - "tzOffset": 10800, - "text": "21:47" - }, - "vehicleId": "codd%5Fnew|1117016%5F430280" - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - }, - "Estimated": { - "value": "1568660538", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|1054576%5F430226" - } - ], - "departureTime": "21:48" - } - }, - { - "lineId": "213_47_trolleybus_mosgortrans", - "name": "т47", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_47_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639568", - "name": "Бескудниковский переулок" - }, - { - "id": "stop__9641903", - "name": "Бескудниковский переулок" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - }, - "Estimated": { - "value": "1568659253", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1112219%5F430329" - }, - { - "Scheduled": { - "value": "1568660940", - "tzOffset": 10800, - "text": "22:09" - }, - "Estimated": { - "value": "1568660519", - "tzOffset": 10800, - "text": "22:01" - }, - "vehicleId": "codd%5Fnew|1139620%5F430382" - }, - { - "Scheduled": { - "value": "1568663580", - "tzOffset": 10800, - "text": "22:53" - } - } - ], - "departureTime": "21:53" - } - } - ], - "threadId": "213B_47_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639568", - "name": "Бескудниковский переулок" - }, - { - "id": "stop__9641903", - "name": "Бескудниковский переулок" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - }, - "Estimated": { - "value": "1568659253", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1112219%5F430329" - }, - { - "Scheduled": { - "value": "1568660940", - "tzOffset": 10800, - "text": "22:09" - }, - "Estimated": { - "value": "1568660519", - "tzOffset": 10800, - "text": "22:01" - }, - "vehicleId": "codd%5Fnew|1139620%5F430382" - }, - { - "Scheduled": { - "value": "1568663580", - "tzOffset": 10800, - "text": "22:53" - } - } - ], - "departureTime": "21:53" - } - }, - { - "lineId": "213_56_trolleybus_mosgortrans", - "name": "т56", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_56_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639561", - "name": "Коровинское шоссе" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660675", - "tzOffset": 10800, - "text": "22:04" - }, - "vehicleId": "codd%5Fnew|146304%5F31207" - } - ], - "Frequency": { - "text": "8 мин", - "value": 480, - "begin": { - "value": "1568606244", - "tzOffset": 10800, - "text": "6:57" - }, - "end": { - "value": "1568670144", - "tzOffset": 10800, - "text": "0:42" - } - } - } - } - ], - "threadId": "213A_56_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639561", - "name": "Коровинское шоссе" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660675", - "tzOffset": 10800, - "text": "22:04" - }, - "vehicleId": "codd%5Fnew|146304%5F31207" - } - ], - "Frequency": { - "text": "8 мин", - "value": 480, - "begin": { - "value": "1568606244", - "tzOffset": 10800, - "text": "6:57" - }, - "end": { - "value": "1568670144", - "tzOffset": 10800, - "text": "0:42" - } - } - } - }, - { - "lineId": "213_63_bus_mosgortrans", - "name": "63", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_63_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|38921%5F9215306" - }, - { - "Estimated": { - "value": "1568660136", - "tzOffset": 10800, - "text": "21:55" - }, - "vehicleId": "codd%5Fnew|38918%5F9215303" - } - ], - "Frequency": { - "text": "17 мин", - "value": 1020, - "begin": { - "value": "1568600987", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568670227", - "tzOffset": 10800, - "text": "0:43" - } - } - } - } - ], - "threadId": "213A_63_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|38921%5F9215306" - }, - { - "Estimated": { - "value": "1568660136", - "tzOffset": 10800, - "text": "21:55" - }, - "vehicleId": "codd%5Fnew|38918%5F9215303" - } - ], - "Frequency": { - "text": "17 мин", - "value": 1020, - "begin": { - "value": "1568600987", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568670227", - "tzOffset": 10800, - "text": "0:43" - } - } - } - }, - { - "lineId": "213_677_bus_mosgortrans", - "name": "677", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_677_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639495", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|11731%5F31376" - } - ], - "Frequency": { - "text": "4 мин", - "value": 240, - "begin": { - "value": "1568600940", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568672640", - "tzOffset": 10800, - "text": "1:24" - } - } - } - } - ], - "threadId": "213B_677_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639495", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|11731%5F31376" - } - ], - "Frequency": { - "text": "4 мин", - "value": 240, - "begin": { - "value": "1568600940", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568672640", - "tzOffset": 10800, - "text": "1:24" - } - } - } - }, - { - "lineId": "213_692_bus_mosgortrans", - "name": "692", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036928706", - "EssentialStops": [ - { - "id": "3163417967", - "name": "Платформа Дегунино" - }, - { - "id": "3163417967", - "name": "Платформа Дегунино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568660280", - "tzOffset": 10800, - "text": "21:58" - }, - "Estimated": { - "value": "1568660255", - "tzOffset": 10800, - "text": "21:57" - }, - "vehicleId": "codd%5Fnew|63029%5F31485" - }, - { - "Scheduled": { - "value": "1568693340", - "tzOffset": 10800, - "text": "7:09" - } - }, - { - "Scheduled": { - "value": "1568696940", - "tzOffset": 10800, - "text": "8:09" - } - } - ], - "departureTime": "21:58" - } - } - ], - "threadId": "2036928706", - "EssentialStops": [ - { - "id": "3163417967", - "name": "Платформа Дегунино" - }, - { - "id": "3163417967", - "name": "Платформа Дегунино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568660280", - "tzOffset": 10800, - "text": "21:58" - }, - "Estimated": { - "value": "1568660255", - "tzOffset": 10800, - "text": "21:57" - }, - "vehicleId": "codd%5Fnew|63029%5F31485" - }, - { - "Scheduled": { - "value": "1568693340", - "tzOffset": 10800, - "text": "7:09" - } - }, - { - "Scheduled": { - "value": "1568696940", - "tzOffset": 10800, - "text": "8:09" - } - } - ], - "departureTime": "21:58" - } - }, - { - "lineId": "213_78_trolleybus_mosgortrans", - "name": "т78", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_78_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9887464", - "name": "9-я Северная линия" - }, - { - "id": "stop__9887464", - "name": "9-я Северная линия" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659620", - "tzOffset": 10800, - "text": "21:47" - }, - "Estimated": { - "value": "1568659898", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|147522%5F31184" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:47" - } - } - ], - "threadId": "213A_78_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9887464", - "name": "9-я Северная линия" - }, - { - "id": "stop__9887464", - "name": "9-я Северная линия" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659620", - "tzOffset": 10800, - "text": "21:47" - }, - "Estimated": { - "value": "1568659898", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|147522%5F31184" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:47" - } - }, - { - "lineId": "213_82_bus_mosgortrans", - "name": "82", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036925244", - "EssentialStops": [ - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - }, - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - } - }, - { - "Scheduled": { - "value": "1568661780", - "tzOffset": 10800, - "text": "22:23" - } - }, - { - "Scheduled": { - "value": "1568663760", - "tzOffset": 10800, - "text": "22:56" - } - } - ], - "departureTime": "21:48" - } - } - ], - "threadId": "2036925244", - "EssentialStops": [ - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - }, - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - } - }, - { - "Scheduled": { - "value": "1568661780", - "tzOffset": 10800, - "text": "22:23" - } - }, - { - "Scheduled": { - "value": "1568663760", - "tzOffset": 10800, - "text": "22:56" - } - } - ], - "departureTime": "21:48" - } - }, - { - "lineId": "2465131598", - "name": "179к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2465131758", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659500", - "tzOffset": 10800, - "text": "21:45" - } - }, - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - } - }, - { - "Scheduled": { - "value": "1568660880", - "tzOffset": 10800, - "text": "22:08" - } - } - ], - "departureTime": "21:45" - } - } - ], - "threadId": "2465131758", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659500", - "tzOffset": 10800, - "text": "21:45" - } - }, - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - } - }, - { - "Scheduled": { - "value": "1568660880", - "tzOffset": 10800, - "text": "22:08" - } - } - ], - "departureTime": "21:45" - } - }, - { - "lineId": "466_bus_default", - "name": "466", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "466B_bus_default", - "EssentialStops": [ - { - "id": "stop__9640546", - "name": "Станция Бескудниково" - }, - { - "id": "stop__9640545", - "name": "Станция Бескудниково" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568604647", - "tzOffset": 10800, - "text": "6:30" - }, - "end": { - "value": "1568675447", - "tzOffset": 10800, - "text": "2:10" - } - } - } - } - ], - "threadId": "466B_bus_default", - "EssentialStops": [ - { - "id": "stop__9640546", - "name": "Станция Бескудниково" - }, - { - "id": "stop__9640545", - "name": "Станция Бескудниково" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568604647", - "tzOffset": 10800, - "text": "6:30" - }, - "end": { - "value": "1568675447", - "tzOffset": 10800, - "text": "2:10" - } - } - } - }, - { - "lineId": "677k_bus_default", - "name": "677к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "677kA_bus_default", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568660003", - "tzOffset": 10800, - "text": "21:53" - }, - "vehicleId": "codd%5Fnew|130308%5F31319" - }, - { - "Scheduled": { - "value": "1568661240", - "tzOffset": 10800, - "text": "22:14" - } - }, - { - "Scheduled": { - "value": "1568662500", - "tzOffset": 10800, - "text": "22:35" - } - } - ], - "departureTime": "21:52" - } - } - ], - "threadId": "677kA_bus_default", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568660003", - "tzOffset": 10800, - "text": "21:53" - }, - "vehicleId": "codd%5Fnew|130308%5F31319" - }, - { - "Scheduled": { - "value": "1568661240", - "tzOffset": 10800, - "text": "22:14" - } - }, - { - "Scheduled": { - "value": "1568662500", - "tzOffset": 10800, - "text": "22:35" - } - } - ], - "departureTime": "21:52" - } - }, - { - "lineId": "m10_bus_default", - "name": "м10", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036926048", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659718", - "tzOffset": 10800, - "text": "21:48" - }, - "vehicleId": "codd%5Fnew|146260%5F31212" - }, - { - "Estimated": { - "value": "1568660422", - "tzOffset": 10800, - "text": "22:00" - }, - "vehicleId": "codd%5Fnew|13997%5F31247" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568606903", - "tzOffset": 10800, - "text": "7:08" - }, - "end": { - "value": "1568675183", - "tzOffset": 10800, - "text": "2:06" - } - } - } - } - ], - "threadId": "2036926048", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659718", - "tzOffset": 10800, - "text": "21:48" - }, - "vehicleId": "codd%5Fnew|146260%5F31212" - }, - { - "Estimated": { - "value": "1568660422", - "tzOffset": 10800, - "text": "22:00" - }, - "vehicleId": "codd%5Fnew|13997%5F31247" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568606903", - "tzOffset": 10800, - "text": "7:08" - }, - "end": { - "value": "1568675183", - "tzOffset": 10800, - "text": "2:06" - } - } - } - } - ] - } - }, - "toponymSeoname": "dmitrovskoye_shosse" - } -} From 6a3132344c8644f11bde973a18b73dcaac8421d7 Mon Sep 17 00:00:00 2001 From: Florian Klien Date: Fri, 20 Sep 2019 15:58:46 +0200 Subject: [PATCH 083/296] Bump openwrt-luci-rpc to version 1.1.1 (#26759) * Revert "luci device-tracker dependency fix (#26215)" This reverts commit 1e61d50fc52d6467565dde34b8d44905204a9093. * bump openwrt-luci-rpc to 1.1.1 fix missing dependency permanent fix for #26215 --- homeassistant/components/luci/manifest.json | 3 +-- requirements_all.txt | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 153f6b5aea6972..dffb4b52667369 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -3,8 +3,7 @@ "name": "Luci", "documentation": "https://www.home-assistant.io/components/luci", "requirements": [ - "openwrt-luci-rpc==1.1.0", - "packaging==19.1" + "openwrt-luci-rpc==1.1.1" ], "dependencies": [], "codeowners": ["@fbradyirl"] diff --git a/requirements_all.txt b/requirements_all.txt index ac49c415398022..522f06bca021fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -913,14 +913,11 @@ opensensemap-api==0.1.5 openwebifpy==3.1.1 # homeassistant.components.luci -openwrt-luci-rpc==1.1.0 +openwrt-luci-rpc==1.1.1 # homeassistant.components.orvibo orvibo==1.1.1 -# homeassistant.components.luci -packaging==19.1 - # homeassistant.components.mqtt # homeassistant.components.shiftr paho-mqtt==1.4.0 From 54242cd65c1d69d88ec366f545313cf003e9cbbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 20 Sep 2019 18:23:34 +0300 Subject: [PATCH 084/296] Type hint additions (#26765) --- .../components/automation/__init__.py | 7 ++++--- homeassistant/components/cover/__init__.py | 15 +++++++------- homeassistant/components/frontend/__init__.py | 9 ++++++--- homeassistant/components/http/ban.py | 6 +++--- .../media_player/reproduce_state.py | 4 ++-- homeassistant/components/switch/light.py | 12 +++++++---- homeassistant/helpers/config_validation.py | 20 +++++++++---------- homeassistant/helpers/script.py | 9 ++++----- homeassistant/helpers/template.py | 18 ++++++++--------- homeassistant/scripts/__init__.py | 12 +++++------ 10 files changed, 60 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 9e08a9cff1fc40..f0529f126f1e73 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -3,6 +3,7 @@ from functools import partial import importlib import logging +from typing import Any import voluptuous as vol @@ -34,7 +35,7 @@ from homeassistant.util.dt import parse_datetime, utcnow -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any DOMAIN = "automation" @@ -281,11 +282,11 @@ async def async_added_to_hass(self) -> None: if enable_automation: await self.async_enable() - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on and update the state.""" await self.async_enable() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self.async_disable() diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index d491765bb00f57..8d2b4430fe110c 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import functools as ft import logging +from typing import Any import voluptuous as vol @@ -33,7 +34,7 @@ ) -# mypy: allow-untyped-calls, allow-incomplete-defs, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -263,7 +264,7 @@ def is_closed(self): """Return if the cover is closed or not.""" raise NotImplementedError() - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" raise NotImplementedError() @@ -274,7 +275,7 @@ def async_open_cover(self, **kwargs): """ return self.hass.async_add_job(ft.partial(self.open_cover, **kwargs)) - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close cover.""" raise NotImplementedError() @@ -285,7 +286,7 @@ def async_close_cover(self, **kwargs): """ return self.hass.async_add_job(ft.partial(self.close_cover, **kwargs)) - def toggle(self, **kwargs) -> None: + def toggle(self, **kwargs: Any) -> None: """Toggle the entity.""" if self.is_closed: self.open_cover(**kwargs) @@ -323,7 +324,7 @@ def async_stop_cover(self, **kwargs): """ return self.hass.async_add_job(ft.partial(self.stop_cover, **kwargs)) - def open_cover_tilt(self, **kwargs): + def open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" pass @@ -334,7 +335,7 @@ def async_open_cover_tilt(self, **kwargs): """ return self.hass.async_add_job(ft.partial(self.open_cover_tilt, **kwargs)) - def close_cover_tilt(self, **kwargs): + def close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" pass @@ -369,7 +370,7 @@ def async_stop_cover_tilt(self, **kwargs): """ return self.hass.async_add_job(ft.partial(self.stop_cover_tilt, **kwargs)) - def toggle_tilt(self, **kwargs) -> None: + def toggle_tilt(self, **kwargs: Any) -> None: """Toggle the entity.""" if self.current_cover_tilt_position == 0: self.open_cover_tilt(**kwargs) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7298ce8c1d086a..8ef662ec878f90 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -4,6 +4,7 @@ import mimetypes import os import pathlib +from typing import Optional, Set, Tuple from aiohttp import web, web_urldispatcher, hdrs import voluptuous as vol @@ -22,7 +23,7 @@ from .storage import async_setup_frontend_storage -# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs # Fix mimetypes for borked Windows machines # https://github.com/home-assistant/home-assistant-polymer/issues/3336 @@ -400,7 +401,9 @@ def url_for(self, **kwargs: str) -> URL: """Construct url for resource with additional params.""" return URL("/") - async def resolve(self, request: web.Request): + async def resolve( + self, request: web.Request + ) -> Tuple[Optional[web_urldispatcher.UrlMappingMatchInfo], Set[str]]: """Resolve resource. Return (UrlMappingMatchInfo, allowed_methods) pair. @@ -447,7 +450,7 @@ def get_template(self): return tpl - async def get(self, request: web.Request): + async def get(self, request: web.Request) -> web.Response: """Serve the index page for panel pages.""" hass = request.app["hass"] diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index d8fa8853c7f18c..7d1e24f369800f 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -18,7 +18,7 @@ from .const import KEY_REAL_IP -# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -165,7 +165,7 @@ def __init__(self, ip_ban: str, banned_at: Optional[datetime] = None) -> None: self.banned_at = banned_at or datetime.utcnow() -async def async_load_ip_bans_config(hass: HomeAssistant, path: str): +async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> List[IpBan]: """Load list of banned IPs from config file.""" ip_list: List[IpBan] = [] @@ -188,7 +188,7 @@ async def async_load_ip_bans_config(hass: HomeAssistant, path: str): return ip_list -def update_ip_bans_config(path: str, ip_ban: IpBan): +def update_ip_bans_config(path: str, ip_ban: IpBan) -> None: """Update config file with new banned IP address.""" with open(path, "a") as out: ip_ = { diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 4eba4657d9554d..dac08afe471dea 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -36,7 +36,7 @@ ) -# mypy: allow-incomplete-defs, allow-untyped-defs +# mypy: allow-untyped-defs async def _async_reproduce_states( @@ -44,7 +44,7 @@ async def _async_reproduce_states( ) -> None: """Reproduce component states.""" - async def call_service(service: str, keys: Iterable): + async def call_service(service: str, keys: Iterable) -> None: """Call service with set of attributes given.""" data = {} data["entity_id"] = state.entity_id diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 2027a8fc458234..8f3b5d87f8c202 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,6 +1,6 @@ """Light support for switch entities.""" import logging -from typing import cast +from typing import cast, Callable, Dict, Optional, Sequence import voluptuous as vol @@ -14,13 +14,14 @@ ) from homeassistant.core import State, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.components.light import PLATFORM_SCHEMA, Light -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,10 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistantType, + config: ConfigType, + async_add_entities: Callable[[Sequence[Entity], bool], None], + discovery_info: Optional[Dict] = None, ) -> None: """Initialize Light Switch platform.""" async_add_entities( @@ -105,7 +109,7 @@ async def async_added_to_hass(self) -> None: @callback def async_state_changed_listener( entity_id: str, old_state: State, new_state: State - ): + ) -> None: """Handle child updates.""" self.async_schedule_update_ha_state(True) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e53954a65ddb0d..952fa41c42c1ce 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -52,7 +52,7 @@ from homeassistant.util import slugify as util_slugify -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any # pylint: disable=invalid-name @@ -95,7 +95,7 @@ def validate(obj: Dict) -> Dict: return validate -def has_at_most_one_key(*keys: str) -> Callable: +def has_at_most_one_key(*keys: str) -> Callable[[Dict], Dict]: """Validate that zero keys exist or one key exists.""" def validate(obj: Dict) -> Dict: @@ -224,7 +224,7 @@ def entity_ids(value: Union[str, List]) -> List[str]: comp_entity_ids = vol.Any(vol.All(vol.Lower, ENTITY_MATCH_ALL), entity_ids) -def entity_domain(domain: str): +def entity_domain(domain: str) -> Callable[[Any], str]: """Validate that entity belong to domain.""" def validate(value: Any) -> str: @@ -235,7 +235,7 @@ def validate(value: Any) -> str: return validate -def entities_domain(domain: str): +def entities_domain(domain: str) -> Callable[[Union[str, List]], List[str]]: """Validate that entities belong to domain.""" def validate(values: Union[str, List]) -> List[str]: @@ -284,7 +284,7 @@ def icon(value): ) -def time(value) -> time_sys: +def time(value: Any) -> time_sys: """Validate and transform a time.""" if isinstance(value, time_sys): return value @@ -300,7 +300,7 @@ def time(value) -> time_sys: return time_val -def date(value) -> date_sys: +def date(value: Any) -> date_sys: """Validate and transform a date.""" if isinstance(value, date_sys): return value @@ -439,7 +439,7 @@ def string(value: Any) -> str: return str(value) -def temperature_unit(value) -> str: +def temperature_unit(value: Any) -> str: """Validate and transform temperature unit.""" value = str(value).upper() if value == "C": @@ -578,7 +578,7 @@ def deprecated( replacement_key: Optional[str] = None, invalidation_version: Optional[str] = None, default: Optional[Any] = None, -): +) -> Callable[[Dict], Dict]: """ Log key as deprecated and provide a replacement (if exists). @@ -626,7 +626,7 @@ def deprecated( " deprecated, please remove it from your configuration" ) - def check_for_invalid_version(value: Optional[Any]): + def check_for_invalid_version(value: Optional[Any]) -> None: """Raise error if current version has reached invalidation.""" if not invalidation_version: return @@ -641,7 +641,7 @@ def check_for_invalid_version(value: Optional[Any]): ) ) - def validator(config: Dict): + def validator(config: Dict) -> Dict: """Check if key is in config and log warning.""" if key in config: value = config[key] diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 0b569e2d4ad1d0..23728b651098aa 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -4,7 +4,7 @@ from contextlib import suppress from datetime import datetime from itertools import islice -from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple +from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple, Any import voluptuous as vol @@ -32,8 +32,7 @@ from homeassistant.util.async_ import run_coroutine_threadsafe, run_callback_threadsafe -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -101,9 +100,9 @@ class Script: def __init__( self, hass: HomeAssistant, - sequence, + sequence: Sequence[Dict[str, Any]], name: Optional[str] = None, - change_listener=None, + change_listener: Optional[Callable[..., Any]] = None, ) -> None: """Initialize the script.""" self.hass = hass diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 98e3849bfb6332..9af1998e894ebe 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -7,7 +7,7 @@ import re from datetime import datetime from functools import wraps -from typing import Iterable +from typing import Any, Iterable import jinja2 from jinja2 import contextfilter, contextfunction @@ -25,13 +25,13 @@ from homeassistant.core import State, callback, split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper -from homeassistant.helpers.typing import TemplateVarsType +from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import convert, dt as dt_util, location as loc_util from homeassistant.util.async_ import run_callback_threadsafe -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any _LOGGER = logging.getLogger(__name__) @@ -106,7 +106,7 @@ def extract_entities(template, variables=None): return MATCH_ALL -def _true(arg) -> bool: +def _true(arg: Any) -> bool: return True @@ -191,7 +191,7 @@ def extract_entities(self, variables=None): """Extract all entities for state_changed listener.""" return extract_entities(self.template, variables) - def render(self, variables: TemplateVarsType = None, **kwargs): + def render(self, variables: TemplateVarsType = None, **kwargs: Any) -> str: """Render given template.""" if variables is not None: kwargs.update(variables) @@ -201,7 +201,7 @@ def render(self, variables: TemplateVarsType = None, **kwargs): ).result() @callback - def async_render(self, variables: TemplateVarsType = None, **kwargs) -> str: + def async_render(self, variables: TemplateVarsType = None, **kwargs: Any) -> str: """Render given template. This method must be run in the event loop. @@ -218,7 +218,7 @@ def async_render(self, variables: TemplateVarsType = None, **kwargs) -> str: @callback def async_render_to_info( - self, variables: TemplateVarsType = None, **kwargs + self, variables: TemplateVarsType = None, **kwargs: Any ) -> RenderInfo: """Render the template and collect an entity filter.""" assert self.hass and _RENDER_INFO not in self.hass.data @@ -479,7 +479,7 @@ def _resolve_state(hass, entity_id_or_state): return None -def expand(hass, *args) -> Iterable[State]: +def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]: """Expand out any groups into entity states.""" search = list(args) found = {} @@ -635,7 +635,7 @@ def distance(hass, *args): ) -def is_state(hass, entity_id: str, state: State) -> bool: +def is_state(hass: HomeAssistantType, entity_id: str, state: State) -> bool: """Test if a state is a specific value.""" state_obj = _get_state(hass, entity_id) return state_obj is not None and state_obj.state == state diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 0a9bac301883b4..00f5984c58ba43 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -5,7 +5,7 @@ import logging import os import sys -from typing import List +from typing import List, Optional, Sequence, Text from homeassistant.bootstrap import async_mount_local_lib_path from homeassistant.config import get_default_config_dir @@ -13,7 +13,7 @@ from homeassistant.util.package import install_package, is_virtual_env, is_installed -# mypy: allow-untyped-defs, allow-incomplete-defs, no-warn-return-any +# mypy: allow-untyped-defs, no-warn-return-any def run(args: List) -> int: @@ -62,13 +62,13 @@ def run(args: List) -> int: return script.run(args[1:]) # type: ignore -def extract_config_dir(args=None) -> str: +def extract_config_dir(args: Optional[Sequence[Text]] = None) -> str: """Extract the config dir from the arguments or get the default.""" parser = argparse.ArgumentParser(add_help=False) parser.add_argument("-c", "--config", default=None) - args = parser.parse_known_args(args)[0] + parsed_args = parser.parse_known_args(args)[0] return ( - os.path.join(os.getcwd(), args.config) - if args.config + os.path.join(os.getcwd(), parsed_args.config) + if parsed_args.config else get_default_config_dir() ) From aaf0f9890d635f1ef9a696af107c935b26e78cc3 Mon Sep 17 00:00:00 2001 From: Askarov Rishat Date: Fri, 20 Sep 2019 19:12:36 +0300 Subject: [PATCH 085/296] Add transport data from maps.yandex.ru api (#26766) * adding feature obtaining Moscow transport data from maps.yandex.ru api * extracting the YandexMapsRequester to pypi * fix code review comments * fix stop_name, state in datetime, logger formating * fix comments * add docstring to init * rename, because it works not only Moscow, but many another big cities in Russia * fix comments * Try to solve relative view in sensor timestamp * back to isoformat * add tests, update external library version * flake8 and black tests for sensor.py * fix manifest.json * update tests, migrate to pytest, async, Using MockDependency * move json to tests/fixtures * script/lint fixes * fix comments * removing check_filter function * fix typo * up version on manifest.json * up version to 0.3.7 in requirements_all.txt --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/yandex_transport/__init__.py | 1 + .../components/yandex_transport/manifest.json | 12 + .../components/yandex_transport/sensor.py | 128 + requirements_all.txt | 3 + tests/components/yandex_transport/__init__.py | 1 + .../test_yandex_transport_sensor.py | 88 + tests/fixtures/yandex_transport_reply.json | 2106 +++++++++++++++++ 9 files changed, 2341 insertions(+) create mode 100644 homeassistant/components/yandex_transport/__init__.py create mode 100644 homeassistant/components/yandex_transport/manifest.json create mode 100644 homeassistant/components/yandex_transport/sensor.py create mode 100644 tests/components/yandex_transport/__init__.py create mode 100644 tests/components/yandex_transport/test_yandex_transport_sensor.py create mode 100644 tests/fixtures/yandex_transport_reply.json diff --git a/.coveragerc b/.coveragerc index 5d5b0f6c81b09b..a29586c7b6e1cd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -747,6 +747,7 @@ omit = homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yamaha/media_player.py homeassistant/components/yamaha_musiccast/media_player.py + homeassistant/components/yandex_transport/* homeassistant/components/yeelight/* homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py diff --git a/CODEOWNERS b/CODEOWNERS index 9766f011889fe9..9bcd475d5d4788 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -318,6 +318,7 @@ homeassistant/components/xiaomi_miio/* @rytilahti @syssi homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yamaha_musiccast/* @jalmeroth +homeassistant/components/yandex_transport/* @rishatik92 homeassistant/components/yeelight/* @rytilahti @zewelor homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yessssms/* @flowolf diff --git a/homeassistant/components/yandex_transport/__init__.py b/homeassistant/components/yandex_transport/__init__.py new file mode 100644 index 00000000000000..d007b2d3df8581 --- /dev/null +++ b/homeassistant/components/yandex_transport/__init__.py @@ -0,0 +1 @@ +"""Service for obtaining information about closer bus from Transport Yandex Service.""" diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json new file mode 100644 index 00000000000000..6c633f848c0c68 --- /dev/null +++ b/homeassistant/components/yandex_transport/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "yandex_transport", + "name": "Yandex Transport", + "documentation": "https://www.home-assistant.io/components/yandex_transport", + "requirements": [ + "ya_ma==0.3.7" + ], + "dependencies": [], + "codeowners": [ + "@rishatik92" + ] +} diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py new file mode 100644 index 00000000000000..340291807ead98 --- /dev/null +++ b/homeassistant/components/yandex_transport/sensor.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +"""Service for obtaining information about closer bus from Transport Yandex Service.""" + +import logging +from datetime import timedelta + +import voluptuous as vol +from ya_ma import YandexMapsRequester + +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +STOP_NAME = "stop_name" +USER_AGENT = "Home Assistant" +ATTRIBUTION = "Data provided by maps.yandex.ru" + +CONF_STOP_ID = "stop_id" +CONF_ROUTE = "routes" + +DEFAULT_NAME = "Yandex Transport" +ICON = "mdi:bus" + +SCAN_INTERVAL = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_STOP_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ROUTE, default=[]): vol.All(cv.ensure_list, [cv.string]), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Yandex transport sensor.""" + stop_id = config[CONF_STOP_ID] + name = config[CONF_NAME] + routes = config[CONF_ROUTE] + + data = YandexMapsRequester(user_agent=USER_AGENT) + add_entities([DiscoverMoscowYandexTransport(data, stop_id, routes, name)], True) + + +class DiscoverMoscowYandexTransport(Entity): + """Implementation of yandex_transport sensor.""" + + def __init__(self, requester, stop_id, routes, name): + """Initialize sensor.""" + self.requester = requester + self._stop_id = stop_id + self._routes = [] + self._routes = routes + self._state = None + self._name = name + self._attrs = None + + def update(self): + """Get the latest data from maps.yandex.ru and update the states.""" + attrs = {} + closer_time = None + try: + yandex_reply = self.requester.get_stop_info(self._stop_id) + data = yandex_reply["data"] + stop_metadata = data["properties"]["StopMetaData"] + except KeyError as key_error: + _LOGGER.warning( + "Exception KeyError was captured, missing key is %s. Yandex returned: %s", + key_error, + yandex_reply, + ) + self.requester.set_new_session() + data = self.requester.get_stop_info(self._stop_id)["data"] + stop_metadata = data["properties"]["StopMetaData"] + stop_name = data["properties"]["name"] + transport_list = stop_metadata["Transport"] + for transport in transport_list: + route = transport["name"] + if self._routes and route not in self._routes: + # skip unnecessary route info + continue + if "Events" in transport["BriefSchedule"]: + for event in transport["BriefSchedule"]["Events"]: + if "Estimated" in event: + posix_time_next = int(event["Estimated"]["value"]) + if closer_time is None or closer_time > posix_time_next: + closer_time = posix_time_next + if route not in attrs: + attrs[route] = [] + attrs[route].append(event["Estimated"]["text"]) + attrs[STOP_NAME] = stop_name + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION + if closer_time is None: + self._state = None + else: + self._state = dt_util.utc_from_timestamp(closer_time).isoformat( + timespec="seconds" + ) + self._attrs = attrs + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 522f06bca021fc..f59ed13bda389b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1991,6 +1991,9 @@ xmltodict==0.12.0 # homeassistant.components.xs1 xs1-api-client==2.3.5 +# homeassistant.components.yandex_transport +ya_ma==0.3.7 + # homeassistant.components.yweather yahooweather==0.10 diff --git a/tests/components/yandex_transport/__init__.py b/tests/components/yandex_transport/__init__.py new file mode 100644 index 00000000000000..fe6b0db52d3e05 --- /dev/null +++ b/tests/components/yandex_transport/__init__.py @@ -0,0 +1 @@ +"""Tests for the yandex transport platform.""" diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py new file mode 100644 index 00000000000000..50d945e7fae371 --- /dev/null +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -0,0 +1,88 @@ +"""Tests for the yandex transport platform.""" + +import json +import pytest + +import homeassistant.components.sensor as sensor +import homeassistant.util.dt as dt_util +from homeassistant.const import CONF_NAME +from tests.common import ( + assert_setup_component, + async_setup_component, + MockDependency, + load_fixture, +) + +REPLY = json.loads(load_fixture("yandex_transport_reply.json")) + + +@pytest.fixture +def mock_requester(): + """Create a mock ya_ma module and YandexMapsRequester.""" + with MockDependency("ya_ma") as ya_ma: + instance = ya_ma.YandexMapsRequester.return_value + instance.get_stop_info.return_value = REPLY + yield instance + + +STOP_ID = 9639579 +ROUTES = ["194", "т36", "т47", "м10"] +NAME = "test_name" +TEST_CONFIG = { + "sensor": { + "platform": "yandex_transport", + "stop_id": 9639579, + "routes": ROUTES, + "name": NAME, + } +} + +FILTERED_ATTRS = { + "т36": ["21:43", "21:47", "22:02"], + "т47": ["21:40", "22:01"], + "м10": ["21:48", "22:00"], + "stop_name": "7-й автобусный парк", + "attribution": "Data provided by maps.yandex.ru", +} + +RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds") + + +async def assert_setup_sensor(hass, config, count=1): + """Set up the sensor and assert it's been created.""" + with assert_setup_component(count): + assert await async_setup_component(hass, sensor.DOMAIN, config) + + +async def test_setup_platform_valid_config(hass, mock_requester): + """Test that sensor is set up properly with valid config.""" + await assert_setup_sensor(hass, TEST_CONFIG) + + +async def test_setup_platform_invalid_config(hass, mock_requester): + """Check an invalid configuration.""" + await assert_setup_sensor( + hass, {"sensor": {"platform": "yandex_transport", "stopid": 1234}}, count=0 + ) + + +async def test_name(hass, mock_requester): + """Return the name if set in the configuration.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + assert state.name == TEST_CONFIG["sensor"][CONF_NAME] + + +async def test_state(hass, mock_requester): + """Return the contents of _state.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + assert state.state == RESULT_STATE + + +async def test_filtered_attributes(hass, mock_requester): + """Return the contents of attributes.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + state_attrs = {key: state.attributes[key] for key in FILTERED_ATTRS} + assert state_attrs == FILTERED_ATTRS diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_reply.json new file mode 100644 index 00000000000000..c5e4857297aa93 --- /dev/null +++ b/tests/fixtures/yandex_transport_reply.json @@ -0,0 +1,2106 @@ +{ + "data": { + "geometries": [ + { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + } + ], + "geometry": { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + }, + "properties": { + "name": "7-й автобусный парк", + "description": "7-й автобусный парк", + "currentTime": "Mon Sep 16 2019 21:40:40 GMT+0300 (Moscow Standard Time)", + "StopMetaData": { + "id": "stop__9639579", + "name": "7-й автобусный парк", + "type": "urban", + "region": { + "id": 213, + "type": 6, + "parent_id": 1, + "capital_id": 0, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow", + "native_name": "", + "iso_name": "RU MOW", + "is_main": true, + "en_name": "Moscow", + "short_en_name": "MSK", + "phone_code": "495 499", + "phone_code_old": "095", + "zip_code": "", + "population": 12506468, + "synonyms": "Moskau, Moskva", + "latitude": 55.753215, + "longitude": 37.622504, + "latitude_size": 0.878654, + "longitude_size": 1.164423, + "zoom": 10, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "weather", + "afisha", + "maps", + "tv", + "ad", + "etrain", + "subway", + "delivery", + "route" + ], + "ename": "moscow", + "bounds": [ + [ + 37.0402925, + 55.31141404514547 + ], + [ + 38.2047155, + 56.190068045145466 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву", + "dative": "Москве", + "directional": "", + "genitive": "Москвы", + "instrumental": "Москвой", + "locative": "", + "nominative": "Москва", + "preposition": "в", + "prepositional": "Москве" + }, + "parent": { + "id": 1, + "type": 5, + "parent_id": 3, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow-and-moscow-oblast", + "native_name": "", + "iso_name": "RU-MOS", + "is_main": true, + "en_name": "Moscow and Moscow Oblast", + "short_en_name": "RU-MOS", + "phone_code": "495 496 498 499", + "phone_code_old": "", + "zip_code": "", + "population": 7503385, + "synonyms": "Московская область, Подмосковье, Podmoskovye", + "latitude": 55.815792, + "longitude": 37.380031, + "latitude_size": 2.705659, + "longitude_size": 5.060749, + "zoom": 8, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 10716, + 10747, + 10758, + 20728, + 10740, + 10738, + 20523, + 10735, + 10734, + 10743, + 21622 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "moscow-and-moscow-oblast", + "bounds": [ + [ + 34.8496565, + 54.439456064325434 + ], + [ + 39.9104055, + 57.14511506432543 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву и Московскую область", + "dative": "Москве и Московской области", + "directional": "", + "genitive": "Москвы и Московской области", + "instrumental": "Москвой и Московской областью", + "locative": "", + "nominative": "Москва и Московская область", + "preposition": "в", + "prepositional": "Москве и Московской области" + }, + "parent": { + "id": 225, + "type": 3, + "parent_id": 10001, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "russia", + "native_name": "", + "iso_name": "RU", + "is_main": false, + "en_name": "Russia", + "short_en_name": "RU", + "phone_code": "7", + "phone_code_old": "", + "zip_code": "", + "population": 146880432, + "synonyms": "Russian Federation,Российская Федерация", + "latitude": 61.698653, + "longitude": 99.505405, + "latitude_size": 40.700127, + "longitude_size": 171.643239, + "zoom": 3, + "tzname": "", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 2, + 65, + 54, + 47, + 43, + 66, + 51, + 56, + 172, + 39, + 62 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "russia", + "bounds": [ + [ + 13.683785499999999, + 35.290400699917846 + ], + [ + -174.6729755, + 75.99052769991785 + ] + ], + "names": { + "ablative": "", + "accusative": "Россию", + "dative": "России", + "directional": "", + "genitive": "России", + "instrumental": "Россией", + "locative": "", + "nominative": "Россия", + "preposition": "в", + "prepositional": "России" + } + } + } + }, + "Transport": [ + { + "lineId": "2036925416", + "name": "194", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659860", + "tzOffset": 10800, + "text": "21:51" + } + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661840", + "tzOffset": 10800, + "text": "22:24" + } + } + ], + "departureTime": "21:51" + } + } + ], + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659860", + "tzOffset": 10800, + "text": "21:51" + } + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661840", + "tzOffset": 10800, + "text": "22:24" + } + } + ], + "departureTime": "21:51" + } + }, + { + "lineId": "213_114_bus_mosgortrans", + "name": "114", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568603405", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1568672165", + "tzOffset": 10800, + "text": "1:16" + } + } + } + } + ], + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568603405", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1568672165", + "tzOffset": 10800, + "text": "1:16" + } + } + } + }, + { + "lineId": "213_154_bus_mosgortrans", + "name": "154", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659260", + "tzOffset": 10800, + "text": "21:41" + }, + "Estimated": { + "value": "1568659252", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1054764%5F191500" + }, + { + "Scheduled": { + "value": "1568660580", + "tzOffset": 10800, + "text": "22:03" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:41" + } + } + ], + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659260", + "tzOffset": 10800, + "text": "21:41" + }, + "Estimated": { + "value": "1568659252", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1054764%5F191500" + }, + { + "Scheduled": { + "value": "1568660580", + "tzOffset": 10800, + "text": "22:03" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:41" + } + }, + { + "lineId": "213_179_bus_mosgortrans", + "name": "179", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568659351", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|59832%5F31359" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661660", + "tzOffset": 10800, + "text": "22:21" + } + } + ], + "departureTime": "21:52" + } + } + ], + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568659351", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|59832%5F31359" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661660", + "tzOffset": 10800, + "text": "22:21" + } + } + ], + "departureTime": "21:52" + } + }, + { + "lineId": "213_191m_minibus_default", + "name": "591", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660525", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|38278%5F9345312" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568602033", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1568672233", + "tzOffset": 10800, + "text": "1:17" + } + } + } + } + ], + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660525", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|38278%5F9345312" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568602033", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1568672233", + "tzOffset": 10800, + "text": "1:17" + } + } + } + }, + { + "lineId": "213_206m_minibus_default", + "name": "206к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568601239", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1568671439", + "tzOffset": 10800, + "text": "1:03" + } + } + } + } + ], + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568601239", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1568671439", + "tzOffset": 10800, + "text": "1:03" + } + } + } + }, + { + "lineId": "213_215_bus_mosgortrans", + "name": "215", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1568601276", + "tzOffset": 10800, + "text": "5:34" + }, + "end": { + "value": "1568671476", + "tzOffset": 10800, + "text": "1:04" + } + } + } + } + ], + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1568601276", + "tzOffset": 10800, + "text": "5:34" + }, + "end": { + "value": "1568671476", + "tzOffset": 10800, + "text": "1:04" + } + } + } + }, + { + "lineId": "213_282_bus_mosgortrans", + "name": "282", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659888", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|34874%5F9345408" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568602180", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1568673460", + "tzOffset": 10800, + "text": "1:37" + } + } + } + } + ], + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659888", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|34874%5F9345408" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568602180", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1568673460", + "tzOffset": 10800, + "text": "1:37" + } + } + } + }, + { + "lineId": "213_294m_minibus_default", + "name": "994", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1568601527", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1568671727", + "tzOffset": 10800, + "text": "1:08" + } + } + } + } + ], + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1568601527", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1568671727", + "tzOffset": 10800, + "text": "1:08" + } + } + } + }, + { + "lineId": "213_36_trolleybus_mosgortrans", + "name": "т36", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + }, + "Estimated": { + "value": "1568659426", + "tzOffset": 10800, + "text": "21:43" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" + }, + { + "Scheduled": { + "value": "1568660520", + "tzOffset": 10800, + "text": "22:02" + }, + "Estimated": { + "value": "1568659656", + "tzOffset": 10800, + "text": "21:47" + }, + "vehicleId": "codd%5Fnew|1117016%5F430280" + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + }, + "Estimated": { + "value": "1568660538", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|1054576%5F430226" + } + ], + "departureTime": "21:48" + } + } + ], + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + }, + "Estimated": { + "value": "1568659426", + "tzOffset": 10800, + "text": "21:43" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" + }, + { + "Scheduled": { + "value": "1568660520", + "tzOffset": 10800, + "text": "22:02" + }, + "Estimated": { + "value": "1568659656", + "tzOffset": 10800, + "text": "21:47" + }, + "vehicleId": "codd%5Fnew|1117016%5F430280" + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + }, + "Estimated": { + "value": "1568660538", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|1054576%5F430226" + } + ], + "departureTime": "21:48" + } + }, + { + "lineId": "213_47_trolleybus_mosgortrans", + "name": "т47", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + }, + "Estimated": { + "value": "1568659253", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1112219%5F430329" + }, + { + "Scheduled": { + "value": "1568660940", + "tzOffset": 10800, + "text": "22:09" + }, + "Estimated": { + "value": "1568660519", + "tzOffset": 10800, + "text": "22:01" + }, + "vehicleId": "codd%5Fnew|1139620%5F430382" + }, + { + "Scheduled": { + "value": "1568663580", + "tzOffset": 10800, + "text": "22:53" + } + } + ], + "departureTime": "21:53" + } + } + ], + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + }, + "Estimated": { + "value": "1568659253", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1112219%5F430329" + }, + { + "Scheduled": { + "value": "1568660940", + "tzOffset": 10800, + "text": "22:09" + }, + "Estimated": { + "value": "1568660519", + "tzOffset": 10800, + "text": "22:01" + }, + "vehicleId": "codd%5Fnew|1139620%5F430382" + }, + { + "Scheduled": { + "value": "1568663580", + "tzOffset": 10800, + "text": "22:53" + } + } + ], + "departureTime": "21:53" + } + }, + { + "lineId": "213_56_trolleybus_mosgortrans", + "name": "т56", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660675", + "tzOffset": 10800, + "text": "22:04" + }, + "vehicleId": "codd%5Fnew|146304%5F31207" + } + ], + "Frequency": { + "text": "8 мин", + "value": 480, + "begin": { + "value": "1568606244", + "tzOffset": 10800, + "text": "6:57" + }, + "end": { + "value": "1568670144", + "tzOffset": 10800, + "text": "0:42" + } + } + } + } + ], + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660675", + "tzOffset": 10800, + "text": "22:04" + }, + "vehicleId": "codd%5Fnew|146304%5F31207" + } + ], + "Frequency": { + "text": "8 мин", + "value": 480, + "begin": { + "value": "1568606244", + "tzOffset": 10800, + "text": "6:57" + }, + "end": { + "value": "1568670144", + "tzOffset": 10800, + "text": "0:42" + } + } + } + }, + { + "lineId": "213_63_bus_mosgortrans", + "name": "63", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|38921%5F9215306" + }, + { + "Estimated": { + "value": "1568660136", + "tzOffset": 10800, + "text": "21:55" + }, + "vehicleId": "codd%5Fnew|38918%5F9215303" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1568600987", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568670227", + "tzOffset": 10800, + "text": "0:43" + } + } + } + } + ], + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|38921%5F9215306" + }, + { + "Estimated": { + "value": "1568660136", + "tzOffset": 10800, + "text": "21:55" + }, + "vehicleId": "codd%5Fnew|38918%5F9215303" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1568600987", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568670227", + "tzOffset": 10800, + "text": "0:43" + } + } + } + }, + { + "lineId": "213_677_bus_mosgortrans", + "name": "677", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|11731%5F31376" + } + ], + "Frequency": { + "text": "4 мин", + "value": 240, + "begin": { + "value": "1568600940", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568672640", + "tzOffset": 10800, + "text": "1:24" + } + } + } + } + ], + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|11731%5F31376" + } + ], + "Frequency": { + "text": "4 мин", + "value": 240, + "begin": { + "value": "1568600940", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568672640", + "tzOffset": 10800, + "text": "1:24" + } + } + } + }, + { + "lineId": "213_692_bus_mosgortrans", + "name": "692", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568660280", + "tzOffset": 10800, + "text": "21:58" + }, + "Estimated": { + "value": "1568660255", + "tzOffset": 10800, + "text": "21:57" + }, + "vehicleId": "codd%5Fnew|63029%5F31485" + }, + { + "Scheduled": { + "value": "1568693340", + "tzOffset": 10800, + "text": "7:09" + } + }, + { + "Scheduled": { + "value": "1568696940", + "tzOffset": 10800, + "text": "8:09" + } + } + ], + "departureTime": "21:58" + } + } + ], + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568660280", + "tzOffset": 10800, + "text": "21:58" + }, + "Estimated": { + "value": "1568660255", + "tzOffset": 10800, + "text": "21:57" + }, + "vehicleId": "codd%5Fnew|63029%5F31485" + }, + { + "Scheduled": { + "value": "1568693340", + "tzOffset": 10800, + "text": "7:09" + } + }, + { + "Scheduled": { + "value": "1568696940", + "tzOffset": 10800, + "text": "8:09" + } + } + ], + "departureTime": "21:58" + } + }, + { + "lineId": "213_78_trolleybus_mosgortrans", + "name": "т78", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659620", + "tzOffset": 10800, + "text": "21:47" + }, + "Estimated": { + "value": "1568659898", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|147522%5F31184" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:47" + } + } + ], + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659620", + "tzOffset": 10800, + "text": "21:47" + }, + "Estimated": { + "value": "1568659898", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|147522%5F31184" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:47" + } + }, + { + "lineId": "213_82_bus_mosgortrans", + "name": "82", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + } + }, + { + "Scheduled": { + "value": "1568661780", + "tzOffset": 10800, + "text": "22:23" + } + }, + { + "Scheduled": { + "value": "1568663760", + "tzOffset": 10800, + "text": "22:56" + } + } + ], + "departureTime": "21:48" + } + } + ], + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + } + }, + { + "Scheduled": { + "value": "1568661780", + "tzOffset": 10800, + "text": "22:23" + } + }, + { + "Scheduled": { + "value": "1568663760", + "tzOffset": 10800, + "text": "22:56" + } + } + ], + "departureTime": "21:48" + } + }, + { + "lineId": "2465131598", + "name": "179к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659500", + "tzOffset": 10800, + "text": "21:45" + } + }, + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + } + }, + { + "Scheduled": { + "value": "1568660880", + "tzOffset": 10800, + "text": "22:08" + } + } + ], + "departureTime": "21:45" + } + } + ], + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659500", + "tzOffset": 10800, + "text": "21:45" + } + }, + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + } + }, + { + "Scheduled": { + "value": "1568660880", + "tzOffset": 10800, + "text": "22:08" + } + } + ], + "departureTime": "21:45" + } + }, + { + "lineId": "466_bus_default", + "name": "466", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568604647", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1568675447", + "tzOffset": 10800, + "text": "2:10" + } + } + } + } + ], + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568604647", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1568675447", + "tzOffset": 10800, + "text": "2:10" + } + } + } + }, + { + "lineId": "677k_bus_default", + "name": "677к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568660003", + "tzOffset": 10800, + "text": "21:53" + }, + "vehicleId": "codd%5Fnew|130308%5F31319" + }, + { + "Scheduled": { + "value": "1568661240", + "tzOffset": 10800, + "text": "22:14" + } + }, + { + "Scheduled": { + "value": "1568662500", + "tzOffset": 10800, + "text": "22:35" + } + } + ], + "departureTime": "21:52" + } + } + ], + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568660003", + "tzOffset": 10800, + "text": "21:53" + }, + "vehicleId": "codd%5Fnew|130308%5F31319" + }, + { + "Scheduled": { + "value": "1568661240", + "tzOffset": 10800, + "text": "22:14" + } + }, + { + "Scheduled": { + "value": "1568662500", + "tzOffset": 10800, + "text": "22:35" + } + } + ], + "departureTime": "21:52" + } + }, + { + "lineId": "m10_bus_default", + "name": "м10", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659718", + "tzOffset": 10800, + "text": "21:48" + }, + "vehicleId": "codd%5Fnew|146260%5F31212" + }, + { + "Estimated": { + "value": "1568660422", + "tzOffset": 10800, + "text": "22:00" + }, + "vehicleId": "codd%5Fnew|13997%5F31247" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568606903", + "tzOffset": 10800, + "text": "7:08" + }, + "end": { + "value": "1568675183", + "tzOffset": 10800, + "text": "2:06" + } + } + } + } + ], + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659718", + "tzOffset": 10800, + "text": "21:48" + }, + "vehicleId": "codd%5Fnew|146260%5F31212" + }, + { + "Estimated": { + "value": "1568660422", + "tzOffset": 10800, + "text": "22:00" + }, + "vehicleId": "codd%5Fnew|13997%5F31247" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568606903", + "tzOffset": 10800, + "text": "7:08" + }, + "end": { + "value": "1568675183", + "tzOffset": 10800, + "text": "2:06" + } + } + } + } + ] + } + }, + "toponymSeoname": "dmitrovskoye_shosse" + } +} From 62adff23f935cbb644c8dfb17e8e109f407374a8 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 20 Sep 2019 15:11:24 -0400 Subject: [PATCH 086/296] ZHA siren and warning device support (#26046) * add ias warning device support * use channel only clusters for warning devices * squawk service * add warning device warning service * update services.yaml * remove debugging statement * update required attr access * fix constant * add error logging to IASWD services --- homeassistant/components/zha/api.py | 130 ++++++++++++++++++ .../components/zha/core/channels/security.py | 96 ++++++++++++- homeassistant/components/zha/core/const.py | 30 ++++ homeassistant/components/zha/services.yaml | 52 +++++++ 4 files changed, 306 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index be079e83fa6bb0..ff9f27d4843c20 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -19,9 +19,16 @@ ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ENDPOINT_ID, + ATTR_LEVEL, ATTR_MANUFACTURER, ATTR_NAME, ATTR_VALUE, + ATTR_WARNING_DEVICE_DURATION, + ATTR_WARNING_DEVICE_MODE, + ATTR_WARNING_DEVICE_STROBE, + ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE, + ATTR_WARNING_DEVICE_STROBE_INTENSITY, + CHANNEL_IAS_WD, CLUSTER_COMMAND_SERVER, CLUSTER_COMMANDS_CLIENT, CLUSTER_COMMANDS_SERVER, @@ -31,6 +38,11 @@ DATA_ZHA_GATEWAY, DOMAIN, MFG_CLUSTER_ID_START, + WARNING_DEVICE_MODE_EMERGENCY, + WARNING_DEVICE_SOUND_HIGH, + WARNING_DEVICE_SQUAWK_MODE_ARMED, + WARNING_DEVICE_STROBE_HIGH, + WARNING_DEVICE_STROBE_YES, ) from .core.helpers import async_is_bindable_target, convert_ieee, get_matched_clusters @@ -56,6 +68,8 @@ SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = "issue_zigbee_cluster_command" SERVICE_DIRECT_ZIGBEE_BIND = "issue_direct_zigbee_bind" SERVICE_DIRECT_ZIGBEE_UNBIND = "issue_direct_zigbee_unbind" +SERVICE_WARNING_DEVICE_SQUAWK = "warning_device_squawk" +SERVICE_WARNING_DEVICE_WARN = "warning_device_warn" SERVICE_ZIGBEE_BIND = "service_zigbee_bind" IEEE_SERVICE = "ieee_based_service" @@ -80,6 +94,41 @@ vol.Optional(ATTR_MANUFACTURER): cv.positive_int, } ), + SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema( + { + vol.Required(ATTR_IEEE): convert_ieee, + vol.Optional( + ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED + ): cv.positive_int, + vol.Optional( + ATTR_WARNING_DEVICE_STROBE, default=WARNING_DEVICE_STROBE_YES + ): cv.positive_int, + vol.Optional( + ATTR_LEVEL, default=WARNING_DEVICE_SOUND_HIGH + ): cv.positive_int, + } + ), + SERVICE_WARNING_DEVICE_WARN: vol.Schema( + { + vol.Required(ATTR_IEEE): convert_ieee, + vol.Optional( + ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY + ): cv.positive_int, + vol.Optional( + ATTR_WARNING_DEVICE_STROBE, default=WARNING_DEVICE_STROBE_YES + ): cv.positive_int, + vol.Optional( + ATTR_LEVEL, default=WARNING_DEVICE_SOUND_HIGH + ): cv.positive_int, + vol.Optional(ATTR_WARNING_DEVICE_DURATION, default=5): cv.positive_int, + vol.Optional( + ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE, default=0x00 + ): cv.positive_int, + vol.Optional( + ATTR_WARNING_DEVICE_STROBE_INTENSITY, default=WARNING_DEVICE_STROBE_HIGH + ): cv.positive_int, + } + ), SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema( { vol.Required(ATTR_IEEE): convert_ieee, @@ -610,6 +659,85 @@ async def issue_zigbee_cluster_command(service): schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND], ) + async def warning_device_squawk(service): + """Issue the squawk command for an IAS warning device.""" + ieee = service.data[ATTR_IEEE] + mode = service.data.get(ATTR_WARNING_DEVICE_MODE) + strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE) + level = service.data.get(ATTR_LEVEL) + + zha_device = zha_gateway.get_device(ieee) + if zha_device is not None: + channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD) + if channel: + await channel.squawk(mode, strobe, level) + else: + _LOGGER.error( + "Squawking IASWD: %s is missing the required IASWD channel!", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + ) + else: + _LOGGER.error( + "Squawking IASWD: %s could not be found!", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + ) + _LOGGER.debug( + "Squawking IASWD: %s %s %s %s", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode), + "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe), + "{}: [{}]".format(ATTR_LEVEL, level), + ) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_WARNING_DEVICE_SQUAWK, + warning_device_squawk, + schema=SERVICE_SCHEMAS[SERVICE_WARNING_DEVICE_SQUAWK], + ) + + async def warning_device_warn(service): + """Issue the warning command for an IAS warning device.""" + ieee = service.data[ATTR_IEEE] + mode = service.data.get(ATTR_WARNING_DEVICE_MODE) + strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE) + level = service.data.get(ATTR_LEVEL) + duration = service.data.get(ATTR_WARNING_DEVICE_DURATION) + duty_mode = service.data.get(ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE) + intensity = service.data.get(ATTR_WARNING_DEVICE_STROBE_INTENSITY) + + zha_device = zha_gateway.get_device(ieee) + if zha_device is not None: + channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD) + if channel: + await channel.start_warning( + mode, strobe, level, duration, duty_mode, intensity + ) + else: + _LOGGER.error( + "Warning IASWD: %s is missing the required IASWD channel!", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + ) + else: + _LOGGER.error( + "Warning IASWD: %s could not be found!", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + ) + _LOGGER.debug( + "Warning IASWD: %s %s %s %s", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode), + "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe), + "{}: [{}]".format(ATTR_LEVEL, level), + ) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_WARNING_DEVICE_WARN, + warning_device_warn, + schema=SERVICE_SCHEMAS[SERVICE_WARNING_DEVICE_WARN], + ) + websocket_api.async_register_command(hass, websocket_permit_devices) websocket_api.async_register_command(hass, websocket_get_devices) websocket_api.async_register_command(hass, websocket_get_device) @@ -629,3 +757,5 @@ def async_unload_api(hass): hass.services.async_remove(DOMAIN, SERVICE_REMOVE) hass.services.async_remove(DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE) hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND) + hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_SQUAWK) + hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_WARN) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index cd407cfc416b68..25c11a9fd4f1cd 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -13,7 +13,15 @@ from . import ZigbeeChannel from .. import registries -from ..const import SIGNAL_ATTR_UPDATED +from ..const import ( + CLUSTER_COMMAND_SERVER, + SIGNAL_ATTR_UPDATED, + WARNING_DEVICE_MODE_EMERGENCY, + WARNING_DEVICE_SOUND_HIGH, + WARNING_DEVICE_SQUAWK_MODE_ARMED, + WARNING_DEVICE_STROBE_HIGH, + WARNING_DEVICE_STROBE_YES, +) _LOGGER = logging.getLogger(__name__) @@ -25,11 +33,95 @@ class IasAce(ZigbeeChannel): pass +@registries.CHANNEL_ONLY_CLUSTERS.register(security.IasWd.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasWd.cluster_id) class IasWd(ZigbeeChannel): """IAS Warning Device channel.""" - pass + @staticmethod + def set_bit(destination_value, destination_bit, source_value, source_bit): + """Set the specified bit in the value.""" + + if IasWd.get_bit(source_value, source_bit): + return destination_value | (1 << destination_bit) + return destination_value + + @staticmethod + def get_bit(value, bit): + """Get the specified bit from the value.""" + return (value & (1 << bit)) != 0 + + async def squawk( + self, + mode=WARNING_DEVICE_SQUAWK_MODE_ARMED, + strobe=WARNING_DEVICE_STROBE_YES, + squawk_level=WARNING_DEVICE_SOUND_HIGH, + ): + """Issue a squawk command. + + This command uses the WD capabilities to emit a quick audible/visible pulse called a + "squawk". The squawk command has no effect if the WD is currently active + (warning in progress). + """ + value = 0 + value = IasWd.set_bit(value, 0, squawk_level, 0) + value = IasWd.set_bit(value, 1, squawk_level, 1) + + value = IasWd.set_bit(value, 3, strobe, 0) + + value = IasWd.set_bit(value, 4, mode, 0) + value = IasWd.set_bit(value, 5, mode, 1) + value = IasWd.set_bit(value, 6, mode, 2) + value = IasWd.set_bit(value, 7, mode, 3) + + await self.device.issue_cluster_command( + self.cluster.endpoint.endpoint_id, + self.cluster.cluster_id, + 0x0001, + CLUSTER_COMMAND_SERVER, + [value], + ) + + async def start_warning( + self, + mode=WARNING_DEVICE_MODE_EMERGENCY, + strobe=WARNING_DEVICE_STROBE_YES, + siren_level=WARNING_DEVICE_SOUND_HIGH, + warning_duration=5, # seconds + strobe_duty_cycle=0x00, + strobe_intensity=WARNING_DEVICE_STROBE_HIGH, + ): + """Issue a start warning command. + + This command starts the WD operation. The WD alerts the surrounding area by audible + (siren) and visual (strobe) signals. + + strobe_duty_cycle indicates the length of the flash cycle. This provides a means + of varying the flash duration for different alarm types (e.g., fire, police, burglar). + Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the + nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second. + The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies + “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for + 6/10ths of a second. + """ + value = 0 + value = IasWd.set_bit(value, 0, siren_level, 0) + value = IasWd.set_bit(value, 1, siren_level, 1) + + value = IasWd.set_bit(value, 2, strobe, 0) + + value = IasWd.set_bit(value, 4, mode, 0) + value = IasWd.set_bit(value, 5, mode, 1) + value = IasWd.set_bit(value, 6, mode, 2) + value = IasWd.set_bit(value, 7, mode, 3) + + await self.device.issue_cluster_command( + self.cluster.endpoint.endpoint_id, + self.cluster.cluster_id, + 0x0000, + CLUSTER_COMMAND_SERVER, + [value, warning_duration, strobe_duty_cycle, strobe_intensity], + ) @registries.BINARY_SENSOR_CLUSTERS.register(security.IasZone.cluster_id) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c35cb168fdff31..ac83c2cdcd8f41 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -34,6 +34,11 @@ ATTR_SIGNATURE = "signature" ATTR_TYPE = "type" ATTR_VALUE = "value" +ATTR_WARNING_DEVICE_DURATION = "duration" +ATTR_WARNING_DEVICE_MODE = "mode" +ATTR_WARNING_DEVICE_STROBE = "strobe" +ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE = "duty_cycle" +ATTR_WARNING_DEVICE_STROBE_INTENSITY = "intensity" BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000] @@ -44,6 +49,7 @@ CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement" CHANNEL_EVENT_RELAY = "event_relay" CHANNEL_FAN = "fan" +CHANNEL_IAS_WD = "ias_wd" CHANNEL_LEVEL = ATTR_LEVEL CHANNEL_ON_OFF = "on_off" CHANNEL_POWER_CONFIGURATION = "power" @@ -177,6 +183,30 @@ def list(cls): UNKNOWN_MANUFACTURER = "unk_manufacturer" UNKNOWN_MODEL = "unk_model" +WARNING_DEVICE_MODE_STOP = 0 +WARNING_DEVICE_MODE_BURGLAR = 1 +WARNING_DEVICE_MODE_FIRE = 2 +WARNING_DEVICE_MODE_EMERGENCY = 3 +WARNING_DEVICE_MODE_POLICE_PANIC = 4 +WARNING_DEVICE_MODE_FIRE_PANIC = 5 +WARNING_DEVICE_MODE_EMERGENCY_PANIC = 6 + +WARNING_DEVICE_STROBE_NO = 0 +WARNING_DEVICE_STROBE_YES = 1 + +WARNING_DEVICE_SOUND_LOW = 0 +WARNING_DEVICE_SOUND_MEDIUM = 1 +WARNING_DEVICE_SOUND_HIGH = 2 +WARNING_DEVICE_SOUND_VERY_HIGH = 3 + +WARNING_DEVICE_STROBE_LOW = 0x00 +WARNING_DEVICE_STROBE_MEDIUM = 0x01 +WARNING_DEVICE_STROBE_HIGH = 0x02 +WARNING_DEVICE_STROBE_VERY_HIGH = 0x03 + +WARNING_DEVICE_SQUAWK_MODE_ARMED = 0 +WARNING_DEVICE_SQUAWK_MODE_DISARMED = 1 + ZHA_DISCOVERY_NEW = "zha_discovery_new_{}" ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" ZHA_GW_MSG = "zha_gateway_message" diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index ffd5aa21472c83..d279af46335fe1 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -82,3 +82,55 @@ issue_zigbee_cluster_command: manufacturer: description: manufacturer code example: 0x00FC + +warning_device_squawk: + description: >- + This service uses the WD capabilities to emit a quick audible/visible pulse called a "squawk". The squawk command has no effect if the WD is currently active (warning in progress). + fields: + ieee: + description: IEEE address for the device + example: "00:0d:6f:00:05:7d:2d:34" + mode: + description: >- + The Squawk Mode field is used as a 4-bit enumeration, and can have one of the values shown in Table 8-24 of the ZCL spec - Squawk Mode Field. The exact operation of each mode (how the WD “squawks”) is implementation specific. + example: 1 + strobe: + description: >- + The strobe field is used as a Boolean, and determines if the visual indication is also required in addition to the audible squawk, as shown in Table 8-25 of the ZCL spec - Strobe Bit. + example: 1 + level: + description: >- + The squawk level field is used as a 2-bit enumeration, and determines the intensity of audible squawk sound as shown in Table 8-26 of the ZCL spec - Squawk Level Field Values. + example: 2 + +warning_device_warn: + description: >- + This service starts the WD operation. The WD alerts the surrounding area by audible (siren) and visual (strobe) signals. + fields: + ieee: + description: IEEE address for the device + example: "00:0d:6f:00:05:7d:2d:34" + mode: + description: >- + The Warning Mode field is used as an 4-bit enumeration, can have one of the values defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards. + example: 1 + strobe: + description: >- + The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated. + example: 1 + level: + description: >- + The Siren Level field is used as a 2-bit enumeration, and indicates the intensity of audible squawk sound as shown in Table 8-22 of the ZCL spec. + example: 2 + duration: + description: >- + Requested duration of warning, in seconds. If both Strobe and Warning Mode are "0" this field SHALL be ignored. + example: 2 + duty_cycle: + description: >- + Indicates the length of the flash cycle. This provides a means of varying the flash duration for different alarm types (e.g., fire, police, burglar). Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second. The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for 6/10ths of a second. + example: 2 + intensity: + description: >- + Indicates the intensity of the strobe as shown in Table 8-23 of the ZCL spec. This attribute is designed to vary the output of the strobe (i.e., brightness) and not its frequency, which is detailed in section 8.4.2.3.1.6 of the ZCL spec. + example: 2 From f6be61c6b75a860384fe75e1b0ef9b801139c2c8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 20 Sep 2019 13:32:41 -0600 Subject: [PATCH 087/296] Bump aiowwlln to 2.0.2 (#26769) --- homeassistant/components/wwlln/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wwlln/manifest.json b/homeassistant/components/wwlln/manifest.json index 6d13f7adbfd497..189b9365105159 100644 --- a/homeassistant/components/wwlln/manifest.json +++ b/homeassistant/components/wwlln/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/wwlln", "requirements": [ - "aiowwlln==2.0.1" + "aiowwlln==2.0.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index f59ed13bda389b..e66f248cbba06f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aioswitcher==2019.4.26 aiounifi==11 # homeassistant.components.wwlln -aiowwlln==2.0.1 +aiowwlln==2.0.2 # homeassistant.components.aladdin_connect aladdin_connect==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 122ff317c0e511..c254da1f9d288f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -73,7 +73,7 @@ aioswitcher==2019.4.26 aiounifi==11 # homeassistant.components.wwlln -aiowwlln==2.0.1 +aiowwlln==2.0.2 # homeassistant.components.ambiclimate ambiclimate==0.2.1 From 5cf9ba51dff7e46c34d2a4dcc02cfabc6d45c2a5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 20 Sep 2019 14:41:46 -0600 Subject: [PATCH 088/296] Bump simplisafe-python to 5.0.1 (#26775) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 8a03ac47402bae..cf26955b207b5e 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/simplisafe", "requirements": [ - "simplisafe-python==4.3.0" + "simplisafe-python==5.0.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index e66f248cbba06f..9d7e8b645b1f5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1738,7 +1738,7 @@ shodan==1.15.0 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==4.3.0 +simplisafe-python==5.0.1 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c254da1f9d288f..fdc9ad8dc3f265 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -386,7 +386,7 @@ ring_doorbell==0.2.3 rxv==0.6.0 # homeassistant.components.simplisafe -simplisafe-python==4.3.0 +simplisafe-python==5.0.1 # homeassistant.components.sleepiq sleepyq==0.7 From 8502f7c7d448cad63eff0a4d2dbc8d5c8d582732 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Sep 2019 17:02:18 -0700 Subject: [PATCH 089/296] Add integration scaffolding script (#26777) * Add integration scaffolding script * Make easier to develop * Update py.test -> pytest --- script/scaffold/__init__.py | 1 + script/scaffold/__main__.py | 56 +++++++++++ script/scaffold/const.py | 5 + script/scaffold/error.py | 10 ++ script/scaffold/gather_info.py | 79 ++++++++++++++++ script/scaffold/generate.py | 47 ++++++++++ script/scaffold/model.py | 12 +++ .../templates/integration/__init__.py | 19 ++++ .../templates/integration/config_flow.py | 57 ++++++++++++ .../scaffold/templates/integration/const.py | 3 + .../scaffold/templates/integration/error.py | 10 ++ .../templates/integration/manifest.json | 11 +++ .../templates/integration/strings.json | 21 +++++ script/scaffold/templates/tests/__init__.py | 1 + .../templates/tests/test_config_flow.py | 93 +++++++++++++++++++ 15 files changed, 425 insertions(+) create mode 100644 script/scaffold/__init__.py create mode 100644 script/scaffold/__main__.py create mode 100644 script/scaffold/const.py create mode 100644 script/scaffold/error.py create mode 100644 script/scaffold/gather_info.py create mode 100644 script/scaffold/generate.py create mode 100644 script/scaffold/model.py create mode 100644 script/scaffold/templates/integration/__init__.py create mode 100644 script/scaffold/templates/integration/config_flow.py create mode 100644 script/scaffold/templates/integration/const.py create mode 100644 script/scaffold/templates/integration/error.py create mode 100644 script/scaffold/templates/integration/manifest.json create mode 100644 script/scaffold/templates/integration/strings.json create mode 100644 script/scaffold/templates/tests/__init__.py create mode 100644 script/scaffold/templates/tests/test_config_flow.py diff --git a/script/scaffold/__init__.py b/script/scaffold/__init__.py new file mode 100644 index 00000000000000..2eca398d998245 --- /dev/null +++ b/script/scaffold/__init__.py @@ -0,0 +1 @@ +"""Scaffold new integration.""" diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py new file mode 100644 index 00000000000000..d1b514ea934a2c --- /dev/null +++ b/script/scaffold/__main__.py @@ -0,0 +1,56 @@ +"""Validate manifests.""" +from pathlib import Path +import subprocess +import sys + +from . import gather_info, generate, error, model + + +def main(): + """Scaffold an integration.""" + if not Path("requirements_all.txt").is_file(): + print("Run from project root") + return 1 + + print("Creating a new integration for Home Assistant.") + + if "--develop" in sys.argv: + print("Running in developer mode. Automatically filling in info.") + print() + + info = model.Info( + domain="develop", + name="Develop Hub", + codeowner="@developer", + requirement="aiodevelop==1.2.3", + ) + else: + try: + info = gather_info.gather_info() + except error.ExitApp as err: + print() + print(err.reason) + return err.exit_code + + generate.generate(info) + + print("Running hassfest to pick up new codeowner and config flow.") + subprocess.run("python -m script.hassfest", shell=True) + print() + + print("Running tests") + print(f"$ pytest tests/components/{info.domain}") + if ( + subprocess.run(f"pytest tests/components/{info.domain}", shell=True).returncode + != 0 + ): + return 1 + print() + + print(f"Successfully created the {info.domain} integration!") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/scaffold/const.py b/script/scaffold/const.py new file mode 100644 index 00000000000000..cf66bb4e2ae4d7 --- /dev/null +++ b/script/scaffold/const.py @@ -0,0 +1,5 @@ +"""Constants for scaffolding.""" +from pathlib import Path + +COMPONENT_DIR = Path("homeassistant/components") +TESTS_DIR = Path("tests/components") diff --git a/script/scaffold/error.py b/script/scaffold/error.py new file mode 100644 index 00000000000000..d99cbe8026aff2 --- /dev/null +++ b/script/scaffold/error.py @@ -0,0 +1,10 @@ +"""Errors for scaffolding.""" + + +class ExitApp(Exception): + """Exception to indicate app should exit.""" + + def __init__(self, reason, exit_code): + """Initialize the exit app exception.""" + self.reason = reason + self.exit_code = exit_code diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py new file mode 100644 index 00000000000000..352d1da206c12b --- /dev/null +++ b/script/scaffold/gather_info.py @@ -0,0 +1,79 @@ +"""Gather info for scaffolding.""" +from homeassistant.util import slugify + +from .const import COMPONENT_DIR +from .model import Info +from .error import ExitApp + + +CHECK_EMPTY = ["Cannot be empty", lambda value: value] + + +FIELDS = { + "domain": { + "prompt": "What is the domain?", + "validators": [ + CHECK_EMPTY, + [ + "Domains cannot contain spaces or special characters.", + lambda value: value == slugify(value), + ], + [ + "There already is an integration with this domain.", + lambda value: not (COMPONENT_DIR / value).exists(), + ], + ], + }, + "name": { + "prompt": "What is the name of your integration?", + "validators": [CHECK_EMPTY], + }, + "codeowner": { + "prompt": "What is your GitHub handle?", + "validators": [ + CHECK_EMPTY, + [ + 'GitHub handles need to start with an "@"', + lambda value: value.startswith("@"), + ], + ], + }, + "requirement": { + "prompt": "What PyPI package and version do you depend on? Leave blank for none.", + "validators": [ + ["Versions should be pinned using '=='.", lambda value: "==" in value] + ], + }, +} + + +def gather_info() -> Info: + """Gather info from user.""" + answers = {} + + for key, info in FIELDS.items(): + hint = None + while key not in answers: + if hint is not None: + print() + print(f"Error: {hint}") + + try: + print() + value = input(info["prompt"] + "\n> ") + except (KeyboardInterrupt, EOFError): + raise ExitApp("Interrupted!", 1) + + value = value.strip() + hint = None + + for validator_hint, validator in info["validators"]: + if not validator(value): + hint = validator_hint + break + + if hint is None: + answers[key] = value + + print() + return Info(**answers) diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py new file mode 100644 index 00000000000000..f7b3f56f2e6623 --- /dev/null +++ b/script/scaffold/generate.py @@ -0,0 +1,47 @@ +"""Generate an integration.""" +import json +from pathlib import Path + +from .const import COMPONENT_DIR, TESTS_DIR +from .model import Info + +TEMPLATE_DIR = Path(__file__).parent / "templates" +TEMPLATE_INTEGRATION = TEMPLATE_DIR / "integration" +TEMPLATE_TESTS = TEMPLATE_DIR / "tests" + + +def generate(info: Info) -> None: + """Generate an integration.""" + print(f"Generating the {info.domain} integration...") + integration_dir = COMPONENT_DIR / info.domain + test_dir = TESTS_DIR / info.domain + + replaces = { + "NEW_DOMAIN": info.domain, + "NEW_NAME": info.name, + "NEW_CODEOWNER": info.codeowner, + # Special case because we need to keep the list empty if there is none. + '"MANIFEST_NEW_REQUIREMENT"': ( + json.dumps(info.requirement) if info.requirement else "" + ), + } + + for src_dir, target_dir in ( + (TEMPLATE_INTEGRATION, integration_dir), + (TEMPLATE_TESTS, test_dir), + ): + # Guard making it for test purposes. + if not target_dir.exists(): + target_dir.mkdir() + + for source_file in src_dir.glob("**/*"): + content = source_file.read_text() + + for to_search, to_replace in replaces.items(): + content = content.replace(to_search, to_replace) + + target_file = target_dir / source_file.relative_to(src_dir) + print(f"Writing {target_file}") + target_file.write_text(content) + + print() diff --git a/script/scaffold/model.py b/script/scaffold/model.py new file mode 100644 index 00000000000000..83fe922d8c4473 --- /dev/null +++ b/script/scaffold/model.py @@ -0,0 +1,12 @@ +"""Models for scaffolding.""" +import attr + + +@attr.s +class Info: + """Info about new integration.""" + + domain: str = attr.ib() + name: str = attr.ib() + codeowner: str = attr.ib() + requirement: str = attr.ib() diff --git a/script/scaffold/templates/integration/__init__.py b/script/scaffold/templates/integration/__init__.py new file mode 100644 index 00000000000000..356c7857d92bdb --- /dev/null +++ b/script/scaffold/templates/integration/__init__.py @@ -0,0 +1,19 @@ +"""The NEW_NAME integration.""" + +from .const import DOMAIN + + +async def async_setup(hass, config): + """Set up the NEW_NAME integration.""" + hass.data[DOMAIN] = config.get(DOMAIN, {}) + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry for NEW_NAME.""" + # TODO forward the entry for each platform that you want to set up. + # hass.async_create_task( + # hass.config_entries.async_forward_entry_setup(entry, "media_player") + # ) + + return True diff --git a/script/scaffold/templates/integration/config_flow.py b/script/scaffold/templates/integration/config_flow.py new file mode 100644 index 00000000000000..c05141ff0b009a --- /dev/null +++ b/script/scaffold/templates/integration/config_flow.py @@ -0,0 +1,57 @@ +"""Config flow for NEW_NAME integration.""" +import logging + +import voluptuous as vol + +from homeassistant import core, config_entries + +from .const import DOMAIN # pylint:disable=unused-import +from .error import CannotConnect, InvalidAuth + +_LOGGER = logging.getLogger(__name__) + +# TODO adjust the data schema to the data that you need +DATA_SCHEMA = vol.Schema({"host": str, "username": str, "password": str}) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + # TODO validate the data can be used to set up a connection. + # If you cannot connect: + # throw CannotConnect + # If the authentication is wrong: + # InvalidAuth + + # Return some info we want to store in the config entry. + return {"title": "Name of the device"} + + +class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for NEW_NAME.""" + + VERSION = 1 + # TODO pick one of the available connection classes + CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + + return self.async_create_entry(title=info["title"], data=user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/script/scaffold/templates/integration/const.py b/script/scaffold/templates/integration/const.py new file mode 100644 index 00000000000000..e8a1c494d49729 --- /dev/null +++ b/script/scaffold/templates/integration/const.py @@ -0,0 +1,3 @@ +"""Constants for the NEW_NAME integration.""" + +DOMAIN = "NEW_DOMAIN" diff --git a/script/scaffold/templates/integration/error.py b/script/scaffold/templates/integration/error.py new file mode 100644 index 00000000000000..a99a32bb9501ab --- /dev/null +++ b/script/scaffold/templates/integration/error.py @@ -0,0 +1,10 @@ +"""Errors for the NEW_NAME integration.""" +from homeassistant.exceptions import HomeAssistantError + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/script/scaffold/templates/integration/manifest.json b/script/scaffold/templates/integration/manifest.json new file mode 100644 index 00000000000000..7c1e141eef07be --- /dev/null +++ b/script/scaffold/templates/integration/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "NEW_DOMAIN", + "name": "NEW_NAME", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/NEW_DOMAIN", + "requirements": ["MANIFEST_NEW_REQUIREMENT"], + "ssdp": {}, + "homekit": {}, + "dependencies": [], + "codeowners": ["NEW_CODEOWNER"] +} diff --git a/script/scaffold/templates/integration/strings.json b/script/scaffold/templates/integration/strings.json new file mode 100644 index 00000000000000..0f29967b286547 --- /dev/null +++ b/script/scaffold/templates/integration/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "NEW_NAME", + "step": { + "user": { + "title": "Connect to the device", + "data": { + "host": "Host" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} diff --git a/script/scaffold/templates/tests/__init__.py b/script/scaffold/templates/tests/__init__.py new file mode 100644 index 00000000000000..081b6d86600012 --- /dev/null +++ b/script/scaffold/templates/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the NEW_NAME integration.""" diff --git a/script/scaffold/templates/tests/test_config_flow.py b/script/scaffold/templates/tests/test_config_flow.py new file mode 100644 index 00000000000000..7735f497f80176 --- /dev/null +++ b/script/scaffold/templates/tests/test_config_flow.py @@ -0,0 +1,93 @@ +"""Test the NEW_NAME config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.components.NEW_DOMAIN.const import DOMAIN +from homeassistant.components.NEW_DOMAIN.error import CannotConnect, InvalidAuth + +from tests.common import mock_coro + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.NEW_DOMAIN.config_flow.validate_input", + return_value=mock_coro({"title": "Test Title"}), + ), patch( + "homeassistant.components.NEW_DOMAIN.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.NEW_DOMAIN.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Test Title" + assert result2["data"] == { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.NEW_DOMAIN.config_flow.validate_input", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.NEW_DOMAIN.config_flow.validate_input", + side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} From 24cbae6ec3bc57e956ca5d52efd56f7e2b023f71 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 21 Sep 2019 00:32:16 +0000 Subject: [PATCH 090/296] [ci skip] Translation update --- .../components/adguard/.translations/pl.json | 2 +- .../cert_expiry/.translations/lb.json | 21 ++++++++++-- .../cert_expiry/.translations/no.json | 2 +- .../cert_expiry/.translations/zh-Hans.json | 16 +++++++++ .../components/deconz/.translations/lb.json | 9 +++++ .../components/deconz/.translations/ru.json | 9 +++-- .../geonetnz_quakes/.translations/lb.json | 17 ++++++++++ .../.translations/zh-Hans.json | 9 +++++ .../.translations/zh-Hans.json | 2 +- .../components/iqvia/.translations/pl.json | 2 +- .../components/izone/.translations/ca.json | 15 +++++++++ .../components/izone/.translations/fr.json | 15 +++++++++ .../components/izone/.translations/ko.json | 15 +++++++++ .../components/izone/.translations/lb.json | 15 +++++++++ .../components/izone/.translations/no.json | 15 +++++++++ .../components/izone/.translations/pl.json | 15 +++++++++ .../components/izone/.translations/ru.json | 15 +++++++++ .../components/life360/.translations/lb.json | 1 + .../linky/.translations/zh-Hans.json | 16 +++++++++ .../components/met/.translations/no.json | 2 +- .../components/met/.translations/zh-Hans.json | 7 +++- .../components/notion/.translations/ru.json | 2 +- .../plaato/.translations/zh-Hans.json | 13 ++++++++ .../components/plex/.translations/ca.json | 33 +++++++++++++++++++ .../components/plex/.translations/fr.json | 33 +++++++++++++++++++ .../components/plex/.translations/ko.json | 33 +++++++++++++++++++ .../components/plex/.translations/lb.json | 33 +++++++++++++++++++ .../components/plex/.translations/no.json | 33 +++++++++++++++++++ .../components/plex/.translations/pl.json | 33 +++++++++++++++++++ .../components/plex/.translations/ru.json | 33 +++++++++++++++++++ .../components/switch/.translations/fr.json | 2 ++ .../components/switch/.translations/no.json | 2 ++ .../components/switch/.translations/pl.json | 2 +- .../components/switch/.translations/ru.json | 2 ++ .../traccar/.translations/zh-Hans.json | 8 +++++ .../twentemilieu/.translations/lb.json | 4 ++- .../twentemilieu/.translations/zh-Hans.json | 7 ++++ .../components/unifi/.translations/lb.json | 18 ++++++++++ .../components/velbus/.translations/lb.json | 3 +- .../velbus/.translations/zh-Hans.json | 14 ++++++++ .../vesync/.translations/zh-Hans.json | 7 ++++ .../components/withings/.translations/fr.json | 3 ++ .../components/withings/.translations/lb.json | 7 ++++ .../components/withings/.translations/no.json | 3 ++ .../withings/.translations/zh-Hans.json | 10 ++++++ 45 files changed, 544 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/cert_expiry/.translations/zh-Hans.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/lb.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json create mode 100644 homeassistant/components/izone/.translations/ca.json create mode 100644 homeassistant/components/izone/.translations/fr.json create mode 100644 homeassistant/components/izone/.translations/ko.json create mode 100644 homeassistant/components/izone/.translations/lb.json create mode 100644 homeassistant/components/izone/.translations/no.json create mode 100644 homeassistant/components/izone/.translations/pl.json create mode 100644 homeassistant/components/izone/.translations/ru.json create mode 100644 homeassistant/components/linky/.translations/zh-Hans.json create mode 100644 homeassistant/components/plaato/.translations/zh-Hans.json create mode 100644 homeassistant/components/plex/.translations/ca.json create mode 100644 homeassistant/components/plex/.translations/fr.json create mode 100644 homeassistant/components/plex/.translations/ko.json create mode 100644 homeassistant/components/plex/.translations/lb.json create mode 100644 homeassistant/components/plex/.translations/no.json create mode 100644 homeassistant/components/plex/.translations/pl.json create mode 100644 homeassistant/components/plex/.translations/ru.json create mode 100644 homeassistant/components/traccar/.translations/zh-Hans.json create mode 100644 homeassistant/components/twentemilieu/.translations/zh-Hans.json create mode 100644 homeassistant/components/velbus/.translations/zh-Hans.json create mode 100644 homeassistant/components/vesync/.translations/zh-Hans.json create mode 100644 homeassistant/components/withings/.translations/zh-Hans.json diff --git a/homeassistant/components/adguard/.translations/pl.json b/homeassistant/components/adguard/.translations/pl.json index e58c901f3643f4..f8f64d542608fe 100644 --- a/homeassistant/components/adguard/.translations/pl.json +++ b/homeassistant/components/adguard/.translations/pl.json @@ -22,7 +22,7 @@ "verify_ssl": "AdGuard Home u\u017cywa odpowiedniego certyfikatu." }, "description": "Skonfiguruj instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i kontrol\u0119.", - "title": "Po\u0142\u0105cz sw\u00f3j AdGuard Home" + "title": "Po\u0142\u0105cz AdGuard Home" } }, "title": "AdGuard Home" diff --git a/homeassistant/components/cert_expiry/.translations/lb.json b/homeassistant/components/cert_expiry/.translations/lb.json index d6811728a22402..9620526e363d9d 100644 --- a/homeassistant/components/cert_expiry/.translations/lb.json +++ b/homeassistant/components/cert_expiry/.translations/lb.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert" + }, "error": { - "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen." - } + "certificate_fetch_failed": "Kann keen Zertifikat vun d\u00ebsen Host a Port recuper\u00e9ieren", + "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen.", + "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert", + "resolve_failed": "D\u00ebsen Host kann net opgel\u00e9ist ginn" + }, + "step": { + "user": { + "data": { + "host": "Den Hostnumm vum Zertifikat", + "name": "De Numm vum Zertifikat", + "port": "De Port vum Zertifikat" + }, + "title": "W\u00e9ieen Zertifikat soll getest ginn" + } + }, + "title": "Zertifikat Verfallsdatum" } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/no.json b/homeassistant/components/cert_expiry/.translations/no.json index e095cc360a0f4e..73e899106c1063 100644 --- a/homeassistant/components/cert_expiry/.translations/no.json +++ b/homeassistant/components/cert_expiry/.translations/no.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Kan ikke hente sertifikat fra denne verts- og portkombinasjonen", - "connection_timeout": "Timeout n\u00e5r det kobles til denne verten", + "connection_timeout": "Tidsavbrudd n\u00e5r du kobler til denne verten", "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert", "resolve_failed": "Denne verten kan ikke l\u00f8ses" }, diff --git a/homeassistant/components/cert_expiry/.translations/zh-Hans.json b/homeassistant/components/cert_expiry/.translations/zh-Hans.json new file mode 100644 index 00000000000000..07affc990a8173 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "connection_timeout": "\u8fde\u63a5\u5230\u6b64\u4e3b\u673a\u65f6\u7684\u8d85\u65f6" + }, + "step": { + "user": { + "data": { + "host": "\u8bc1\u4e66\u7684\u4e3b\u673a\u540d", + "name": "\u8bc1\u4e66\u7684\u540d\u79f0", + "port": "\u8bc1\u4e66\u7684\u7aef\u53e3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index c536e5771411f9..1a03143f11edfd 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -49,6 +49,8 @@ "button_3": "Dr\u00ebtte Kn\u00e4ppchen", "button_4": "V\u00e9ierte Kn\u00e4ppchen", "close": "Zoumaachen", + "dim_down": "Erhellen", + "dim_up": "Verd\u00e4ischteren", "left": "L\u00e9nks", "open": "Op", "right": "Riets", @@ -70,6 +72,13 @@ }, "options": { "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ Clip Sensoren erlaben", + "allow_deconz_groups": "deCONZ Luucht Gruppen erlaben" + }, + "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren" + }, "deconz_devices": { "data": { "allow_clip_sensor": "deCONZ Clip Sensoren erlaben", diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 92fd1e3e7490c6..612c5afd033b79 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -49,9 +49,13 @@ "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", + "dim_down": "\u0423\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", + "dim_up": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", "left": "\u041d\u0430\u043b\u0435\u0432\u043e", "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", - "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e" + "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c" }, "trigger_type": { "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b", @@ -62,7 +66,8 @@ "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430", "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", - "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b" + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438" } }, "options": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/lb.json b/homeassistant/components/geonetnz_quakes/.translations/lb.json new file mode 100644 index 00000000000000..2499befecbb822 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Standuert ass scho registr\u00e9iert" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "F\u00ebllt \u00e4r Filter D\u00e9tailer aus." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json b/homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json new file mode 100644 index 00000000000000..3786b03f41fc3e --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "\u586b\u5199\u60a8\u7684filter\u8be6\u7ec6\u4fe1\u606f\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hans.json b/homeassistant/components/homekit_controller/.translations/zh-Hans.json index 8d064622f7e468..d9fdc8f91c2e1f 100644 --- a/homeassistant/components/homekit_controller/.translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/.translations/zh-Hans.json @@ -23,7 +23,7 @@ "data": { "pairing_code": "\u914d\u5bf9\u4ee3\u7801" }, - "description": "\u8f93\u5165\u60a8\u7684 HomeKit \u914d\u5bf9\u4ee3\u7801\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6", + "description": "\u8f93\u5165\u60a8\u7684HomeKit\u914d\u5bf9\u4ee3\u7801\uff08\u683c\u5f0f\u4e3aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6", "title": "\u4e0e HomeKit \u914d\u4ef6\u914d\u5bf9" }, "user": { diff --git a/homeassistant/components/iqvia/.translations/pl.json b/homeassistant/components/iqvia/.translations/pl.json index 7a6e9a8a915634..b528cdeb70f3b0 100644 --- a/homeassistant/components/iqvia/.translations/pl.json +++ b/homeassistant/components/iqvia/.translations/pl.json @@ -9,7 +9,7 @@ "data": { "zip_code": "Kod pocztowy" }, - "description": "Wprowad\u017a sw\u00f3j ameryka\u0144ski lub kanadyjski kod pocztowy.", + "description": "Wprowad\u017a ameryka\u0144ski lub kanadyjski kod pocztowy.", "title": "IQVIA" } }, diff --git a/homeassistant/components/izone/.translations/ca.json b/homeassistant/components/izone/.translations/ca.json new file mode 100644 index 00000000000000..b80d9bee4e271a --- /dev/null +++ b/homeassistant/components/izone/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius iZone a la xarxa.", + "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de iZone." + }, + "step": { + "confirm": { + "description": "Vols configurar iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/fr.json b/homeassistant/components/izone/.translations/fr.json new file mode 100644 index 00000000000000..c90416b0619746 --- /dev/null +++ b/homeassistant/components/izone/.translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun p\u00e9riph\u00e9rique iZone trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Une seule configuration d'iZone est n\u00e9cessaire." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/ko.json b/homeassistant/components/izone/.translations/ko.json new file mode 100644 index 00000000000000..69b8ce8a31ea35 --- /dev/null +++ b/homeassistant/components/izone/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "iZone \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 iZone \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "iZone \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/lb.json b/homeassistant/components/izone/.translations/lb.json new file mode 100644 index 00000000000000..c6e075683ad159 --- /dev/null +++ b/homeassistant/components/izone/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng iZone Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun iZone ass n\u00e9ideg." + }, + "step": { + "confirm": { + "description": "Soll iZone konfigur\u00e9iert ginn?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/no.json b/homeassistant/components/izone/.translations/no.json new file mode 100644 index 00000000000000..fcd5c1df019a6b --- /dev/null +++ b/homeassistant/components/izone/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Finner ingen iZone-enheter p\u00e5 nettverket.", + "single_instance_allowed": "Bare en enkelt konfigurasjon av iZone er n\u00f8dvendig." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/pl.json b/homeassistant/components/izone/.translations/pl.json new file mode 100644 index 00000000000000..4f90cf71cbcb04 --- /dev/null +++ b/homeassistant/components/izone/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 iZone.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja iZone." + }, + "step": { + "confirm": { + "description": "Chcesz skonfigurowa\u0107 iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/ru.json b/homeassistant/components/izone/.translations/ru.json new file mode 100644 index 00000000000000..7e632c8dd62119 --- /dev/null +++ b/homeassistant/components/izone/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 iZone \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/lb.json b/homeassistant/components/life360/.translations/lb.json index bfed5937e24bea..3af9ab00728e5d 100644 --- a/homeassistant/components/life360/.translations/lb.json +++ b/homeassistant/components/life360/.translations/lb.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ong\u00eblteg Login Informatioune", "invalid_username": "Ong\u00ebltege Benotzernumm", + "unexpected": "Onerwaarte Feeler bei der Kommunikatioun mam Life360 Server", "user_already_configured": "Kont ass scho konfigur\u00e9iert" }, "step": { diff --git a/homeassistant/components/linky/.translations/zh-Hans.json b/homeassistant/components/linky/.translations/zh-Hans.json new file mode 100644 index 00000000000000..b450a3cbdb08c7 --- /dev/null +++ b/homeassistant/components/linky/.translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "username_exists": "\u8d26\u6237\u5df2\u914d\u7f6e\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7535\u5b50\u90ae\u7bb1" + }, + "description": "\u8f93\u5165\u60a8\u7684\u8eab\u4efd\u8ba4\u8bc1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/no.json b/homeassistant/components/met/.translations/no.json index 6ebaa08457f653..9a3ef350ab1082 100644 --- a/homeassistant/components/met/.translations/no.json +++ b/homeassistant/components/met/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Navnet eksisterer allerede" + "name_exists": "Lokasjonen finnes allerede" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/zh-Hans.json b/homeassistant/components/met/.translations/zh-Hans.json index 9565bb666181d4..9027347174d3e7 100644 --- a/homeassistant/components/met/.translations/zh-Hans.json +++ b/homeassistant/components/met/.translations/zh-Hans.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "name_exists": "\u4f4d\u7f6e\u5df2\u5b58\u5728" + }, "step": { "user": { "data": { + "elevation": "\u6d77\u62d4", "latitude": "\u7eac\u5ea6", - "longitude": "\u7ecf\u5ea6" + "longitude": "\u7ecf\u5ea6", + "name": "\u540d\u79f0" }, "title": "\u4f4d\u7f6e" } diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index f43fbeb58b7b0e..c7e89c368c178c 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -3,7 +3,7 @@ "error": { "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c", - "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b" + "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e" }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/zh-Hans.json b/homeassistant/components/plaato/.translations/zh-Hans.json new file mode 100644 index 00000000000000..8d5c25babfab46 --- /dev/null +++ b/homeassistant/components/plaato/.translations/zh-Hans.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u53ef\u4ece\u4e92\u8054\u7f51\u8bbf\u95ee\u4ee5\u63a5\u6536Plaato Airlock\u6d88\u606f\u3002" + }, + "step": { + "user": { + "description": "\u4f60\u786e\u5b9a\u8981\u8bbe\u7f6ePlaato Airlock\u5417\uff1f", + "title": "\u8bbe\u7f6ePlaato Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json new file mode 100644 index 00000000000000..eb4f6459f4dcbe --- /dev/null +++ b/homeassistant/components/plex/.translations/ca.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "Tots els servidors enlla\u00e7ats ja estan configurats", + "already_configured": "Aquest servidor Plex ja est\u00e0 configurat", + "already_in_progress": "S\u2019est\u00e0 configurant Plex", + "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", + "unknown": "Ha fallat per motiu desconegut" + }, + "error": { + "faulty_credentials": "Ha fallat l'autoritzaci\u00f3", + "no_servers": "No hi ha servidors enlla\u00e7ats amb el compte", + "not_found": "No s'ha trobat el servidor Plex" + }, + "step": { + "select_server": { + "data": { + "server": "Servidor" + }, + "description": "Hi ha diversos servidors disponibles, selecciona'n un:", + "title": "Selecciona servidor Plex" + }, + "user": { + "data": { + "token": "Testimoni d'autenticaci\u00f3 Plex" + }, + "description": "Introdueix un testimoni d'autenticaci\u00f3 Plex per configurar-ho autom\u00e0ticament.", + "title": "Connexi\u00f3 amb el servidor Plex" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json new file mode 100644 index 00000000000000..58a5169ac02701 --- /dev/null +++ b/homeassistant/components/plex/.translations/fr.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "Tous les serveurs li\u00e9s sont d\u00e9j\u00e0 configur\u00e9s", + "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Plex en cours de configuration", + "invalid_import": "La configuration import\u00e9e est invalide", + "unknown": "\u00c9chec pour une raison inconnue" + }, + "error": { + "faulty_credentials": "L'autorisation \u00e0 \u00e9chou\u00e9e", + "no_servers": "Aucun serveur li\u00e9 au compte", + "not_found": "Serveur Plex introuvable" + }, + "step": { + "select_server": { + "data": { + "server": "Serveur" + }, + "description": "Plusieurs serveurs disponibles, s\u00e9lectionnez-en un:", + "title": "S\u00e9lectionnez le serveur Plex" + }, + "user": { + "data": { + "token": "Jeton plex" + }, + "description": "Entrez un jeton Plex pour la configuration automatique.", + "title": "Connecter un serveur Plex" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json new file mode 100644 index 00000000000000..d2610c68aed74f --- /dev/null +++ b/homeassistant/components/plex/.translations/ko.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "\uc774\ubbf8 \uad6c\uc131\ub41c \ubaa8\ub4e0 \uc5f0\uacb0\ub41c \uc11c\ubc84", + "already_configured": "\uc774 Plex \uc11c\ubc84\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4", + "invalid_import": "\uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc774\uc720\ub85c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "faulty_credentials": "\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4", + "no_servers": "\uacc4\uc815\uc5d0 \uc5f0\uacb0\ub41c \uc11c\ubc84\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "not_found": "Plex \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "select_server": { + "data": { + "server": "\uc11c\ubc84" + }, + "description": "\uc5ec\ub7ec \uc11c\ubc84\uac00 \uc0ac\uc6a9 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:", + "title": "Plex \uc11c\ubc84 \uc120\ud0dd" + }, + "user": { + "data": { + "token": "Plex \ud1a0\ud070" + }, + "description": "\uc790\ub3d9 \uc124\uc815\uc744 \uc704\ud574 Plex \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json new file mode 100644 index 00000000000000..130cf2067abe72 --- /dev/null +++ b/homeassistant/components/plex/.translations/lb.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "All verbonne Server sinn scho konfigur\u00e9iert", + "already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert", + "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", + "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", + "unknown": "Onbekannte Feeler opgetrueden" + }, + "error": { + "faulty_credentials": "Feeler beider Autorisatioun", + "no_servers": "Kee Server as mam Kont verbonnen", + "not_found": "Kee Plex Server fonnt" + }, + "step": { + "select_server": { + "data": { + "server": "Server" + }, + "description": "M\u00e9i Server disponibel, wielt een aus:", + "title": "Plex Server auswielen" + }, + "user": { + "data": { + "token": "Jeton fir de Plex" + }, + "description": "Gitt een Jeton fir de Plex un fir eng automatesch Konfiguratioun", + "title": "Plex Server verbannen" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json new file mode 100644 index 00000000000000..8ac90efe3d1474 --- /dev/null +++ b/homeassistant/components/plex/.translations/no.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "Alle knyttet servere som allerede er konfigurert", + "already_configured": "Denne Plex-serveren er allerede konfigurert", + "already_in_progress": "Plex blir konfigurert", + "invalid_import": "Den importerte konfigurasjonen er ugyldig", + "unknown": "Mislyktes av ukjent \u00e5rsak" + }, + "error": { + "faulty_credentials": "Autorisasjonen mislyktes", + "no_servers": "Ingen servere koblet til kontoen", + "not_found": "Plex-server ikke funnet" + }, + "step": { + "select_server": { + "data": { + "server": "Server" + }, + "description": "Flere servere tilgjengelig, velg en:", + "title": "Velg Plex-server" + }, + "user": { + "data": { + "token": "Plex token" + }, + "description": "Legg inn et Plex-token for automatisk oppsett.", + "title": "Koble til Plex-server" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json new file mode 100644 index 00000000000000..606f97d6965c60 --- /dev/null +++ b/homeassistant/components/plex/.translations/pl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.", + "already_configured": "Serwer Plex jest ju\u017c skonfigurowany", + "already_in_progress": "Plex jest konfigurowany", + "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", + "unknown": "Nieznany b\u0142\u0105d" + }, + "error": { + "faulty_credentials": "Autoryzacja nie powiod\u0142a si\u0119", + "no_servers": "Brak serwer\u00f3w po\u0142\u0105czonych z kontem", + "not_found": "Nie znaleziono serwera Plex" + }, + "step": { + "select_server": { + "data": { + "server": "Serwer" + }, + "description": "Dost\u0119pnych jest wiele serwer\u00f3w, wybierz jeden:", + "title": "Wybierz serwer Plex" + }, + "user": { + "data": { + "token": "Token Plex" + }, + "description": "Wprowad\u017a token Plex do automatycznej konfiguracji.", + "title": "Po\u0142\u0105cz z serwerem Plex" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json new file mode 100644 index 00000000000000..46cd613df4ac70 --- /dev/null +++ b/homeassistant/components/plex/.translations/ru.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", + "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", + "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435" + }, + "error": { + "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", + "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e", + "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d" + }, + "step": { + "select_server": { + "data": { + "server": "\u0421\u0435\u0440\u0432\u0435\u0440" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u0438\u043d \u0438\u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432:", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex" + }, + "user": { + "data": { + "token": "\u0422\u043e\u043a\u0435\u043d" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d Plex \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "title": "Plex" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/fr.json b/homeassistant/components/switch/.translations/fr.json index 4775d62bce3758..807b85c5fb56d6 100644 --- a/homeassistant/components/switch/.translations/fr.json +++ b/homeassistant/components/switch/.translations/fr.json @@ -6,6 +6,8 @@ "turn_on": "Allumer {entity_name}" }, "condition_type": { + "is_off": "{entity_name} est \u00e9teint", + "is_on": "{entity_name} est allum\u00e9", "turn_off": "{entity_name} \u00e9teint", "turn_on": "{entity_name} allum\u00e9" }, diff --git a/homeassistant/components/switch/.translations/no.json b/homeassistant/components/switch/.translations/no.json index adc128991c5efc..3469079f230b43 100644 --- a/homeassistant/components/switch/.translations/no.json +++ b/homeassistant/components/switch/.translations/no.json @@ -6,6 +6,8 @@ "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" }, "condition_type": { + "is_off": "{entity_name} er av", + "is_on": "{entity_name} er p\u00e5", "turn_off": "{entity_name} sl\u00e5tt av", "turn_on": "{entity_name} sl\u00e5tt p\u00e5" }, diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index 921048286b6a82..199b150f68ee6d 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -6,7 +6,7 @@ "turn_on": "W\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "(entity_name} jest wy\u0142\u0105czony.", + "is_off": "{entity_name} jest wy\u0142\u0105czony.", "is_on": "{entity_name} jest w\u0142\u0105czony", "turn_off": "{entity_name} wy\u0142\u0105czone", "turn_on": "{entity_name} w\u0142\u0105czone" diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json index 45a941b665de03..b769e56c97446a 100644 --- a/homeassistant/components/switch/.translations/ru.json +++ b/homeassistant/components/switch/.translations/ru.json @@ -6,6 +6,8 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" }, "condition_type": { + "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, diff --git a/homeassistant/components/traccar/.translations/zh-Hans.json b/homeassistant/components/traccar/.translations/zh-Hans.json new file mode 100644 index 00000000000000..248e8f9f44ed89 --- /dev/null +++ b/homeassistant/components/traccar/.translations/zh-Hans.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u53ef\u4ece\u4e92\u8054\u7f51\u8bbf\u95ee\u4ee5\u63a5\u6536Traccar\u6d88\u606f\u3002", + "one_instance_allowed": "\u53ea\u6709\u4e00\u4e2a\u5b9e\u4f8b\u662f\u5fc5\u9700\u7684\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/lb.json b/homeassistant/components/twentemilieu/.translations/lb.json index 0b07c5003ef2ff..b6f10842b4d1c7 100644 --- a/homeassistant/components/twentemilieu/.translations/lb.json +++ b/homeassistant/components/twentemilieu/.translations/lb.json @@ -4,7 +4,8 @@ "address_exists": "Adresse ass scho ageriicht." }, "error": { - "connection_error": "Feeler beim verbannen." + "connection_error": "Feeler beim verbannen.", + "invalid_address": "Adresse net am Twente Milieu Service Ber\u00e4ich fonnt" }, "step": { "user": { @@ -13,6 +14,7 @@ "house_number": "Haus Nummer", "post_code": "Postleitzuel" }, + "description": "Offallsammlung Informatiounen vun Twente Milieu zu \u00e4erer Adresse ariichten.", "title": "Twente Milieu" } }, diff --git a/homeassistant/components/twentemilieu/.translations/zh-Hans.json b/homeassistant/components/twentemilieu/.translations/zh-Hans.json new file mode 100644 index 00000000000000..80301cfd57b808 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "address_exists": "\u5730\u5740\u5df2\u7ecf\u8bbe\u7f6e\u597d\u4e86\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json index 3bef273b83e53f..05b0ffc0c44cdf 100644 --- a/homeassistant/components/unifi/.translations/lb.json +++ b/homeassistant/components/unifi/.translations/lb.json @@ -22,5 +22,23 @@ } }, "title": "Unifi Kontroller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Z\u00e4it a Sekonne vum leschten Z\u00e4itpunkt un bis den Apparat als \u00ebnnerwee consider\u00e9iert g\u00ebtt", + "track_clients": "Netzwierk Cliente verfollegen", + "track_devices": "Netzwierk Apparater (Ubiquiti Apparater) verfollegen", + "track_wired_clients": "Kabel Netzwierk Cliente abez\u00e9ien" + } + }, + "init": { + "data": { + "one": "Een", + "other": "M\u00e9i" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/lb.json b/homeassistant/components/velbus/.translations/lb.json index 89e0bd818d277c..f38a74e5c1fd19 100644 --- a/homeassistant/components/velbus/.translations/lb.json +++ b/homeassistant/components/velbus/.translations/lb.json @@ -12,7 +12,8 @@ "data": { "name": "Numm fir d\u00ebs velbus Verbindung", "port": "Verbindungs zeeche-folleg" - } + }, + "title": "D\u00e9fin\u00e9iert den Typ vun der Velbus Verbindung" } }, "title": "Velbus Interface" diff --git a/homeassistant/components/velbus/.translations/zh-Hans.json b/homeassistant/components/velbus/.translations/zh-Hans.json new file mode 100644 index 00000000000000..7b2bc3b028b78d --- /dev/null +++ b/homeassistant/components/velbus/.translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "port_exists": "\u6b64\u7aef\u53e3\u5df2\u914d\u7f6e\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "name": "\u8fd9\u4e2avelbus\u8fde\u63a5\u7684\u540d\u79f0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/zh-Hans.json b/homeassistant/components/vesync/.translations/zh-Hans.json new file mode 100644 index 00000000000000..caa00f36c89435 --- /dev/null +++ b/homeassistant/components/vesync/.translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_setup": "\u53ea\u5141\u8bb8\u4e00\u4e2aVesync\u5b9e\u4f8b" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json index b66786cc9e0e18..ad715d54eb1df5 100644 --- a/homeassistant/components/withings/.translations/fr.json +++ b/homeassistant/components/withings/.translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Vous devez configurer Withings avant de pouvoir vous authentifier avec celui-ci. Veuillez lire la documentation." + }, "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." }, diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json index 9015f4908308d1..5ca969f039102b 100644 --- a/homeassistant/components/withings/.translations/lb.json +++ b/homeassistant/components/withings/.translations/lb.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "no_flows": "Dir musst Withingss konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen. Liest w.e.g. d'Instruktioune." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich mam ausgewielte Profile mat Withings authentifiz\u00e9iert." + }, "step": { "user": { "data": { "profile": "Profil" }, + "description": "Wielt ee Benotzer Profile aus dee mam Withings Profile soll verbonne ginn. Stellt s\u00e9cher dass dir op der Withings S\u00e4it deeselwechte Benotzer auswielt, soss ginn d'Donn\u00e9e net richteg ugewisen.", "title": "Benotzer Profil." } }, diff --git a/homeassistant/components/withings/.translations/no.json b/homeassistant/components/withings/.translations/no.json index 22d8884d66a573..d32c9640fd7636 100644 --- a/homeassistant/components/withings/.translations/no.json +++ b/homeassistant/components/withings/.translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Du m\u00e5 konfigurere Withings f\u00f8r du kan godkjenne med den. Vennligst les dokumentasjonen." + }, "create_entry": { "default": "Vellykket autentisering for Withings og den valgte profilen." }, diff --git a/homeassistant/components/withings/.translations/zh-Hans.json b/homeassistant/components/withings/.translations/zh-Hans.json new file mode 100644 index 00000000000000..c7485b09248ccd --- /dev/null +++ b/homeassistant/components/withings/.translations/zh-Hans.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "\u8bf7\u9009\u62e9\u4f60\u60f3\u8981Home Assistant\u548cWithings\u5bf9\u5e94\u7684\u7528\u6237\u914d\u7f6e\u6587\u4ef6\u3002\u5728Withings\u9875\u9762\u4e0a\uff0c\u8bf7\u52a1\u5fc5\u9009\u62e9\u76f8\u540c\u7684\u7528\u6237\uff0c\u5426\u5219\u6570\u636e\u5c06\u65e0\u6cd5\u6b63\u786e\u6807\u8bb0\u3002", + "title": "\u7528\u6237\u8d44\u6599" + } + } + } +} \ No newline at end of file From ed21019b7a4c746b443b7d1192db11880da06c90 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 21 Sep 2019 14:34:08 +0100 Subject: [PATCH 091/296] Bump HAP-python to 2.6.0 for homekit (#26783) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index ea3e801ac536f4..ebb0895bd7a100 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "Homekit", "documentation": "https://www.home-assistant.io/components/homekit", "requirements": [ - "HAP-python==2.5.0" + "HAP-python==2.6.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 9d7e8b645b1f5c..18ca056584653f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,7 +35,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.0.0 # homeassistant.components.homekit -HAP-python==2.5.0 +HAP-python==2.6.0 # homeassistant.components.mastodon Mastodon.py==1.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdc9ad8dc3f265..621863c8efdc5f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -23,7 +23,7 @@ requests_mock==1.6.0 # homeassistant.components.homekit -HAP-python==2.5.0 +HAP-python==2.6.0 # homeassistant.components.mobile_app # homeassistant.components.owntracks From 0e157856027d854e4183e00d91c830bfcdad877f Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Sat, 21 Sep 2019 09:56:40 -0400 Subject: [PATCH 092/296] Bump pynws version to 0.8.1 (#26770) * Bump to version 0.8.1 Fixes #26753. * gen_requirements.py changes * fix default params change in tests --- homeassistant/components/nws/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nws/test_weather.py | 21 +++++---------------- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index b0e5fdb208844a..bad90d9e827d77 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/components/nws", "dependencies": [], "codeowners": ["@MatthewFlamm"], - "requirements": ["pynws==0.7.4"] + "requirements": ["pynws==0.8.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 18ca056584653f..ef036c99c70f27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1339,7 +1339,7 @@ pynuki==1.3.3 pynut2==2.1.2 # homeassistant.components.nws -pynws==0.7.4 +pynws==0.8.1 # homeassistant.components.nx584 pynx584==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 621863c8efdc5f..a8aa92c81ddbbc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -312,7 +312,7 @@ pymfy==0.5.2 pymonoprice==0.3 # homeassistant.components.nws -pynws==0.7.4 +pynws==0.8.1 # homeassistant.components.nx584 pynx584==0.4 diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index 436d25750fc518..0e450f06238d94 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -110,9 +110,7 @@ async def test_imperial(hass, aioclient_mock): STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") ) aioclient_mock.get( - OBSURL.format("KMIE"), - text=load_fixture("nws-weather-obs-valid.json"), - params={"limit": 1}, + OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json") ) aioclient_mock.get( FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") @@ -142,9 +140,7 @@ async def test_metric(hass, aioclient_mock): STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") ) aioclient_mock.get( - OBSURL.format("KMIE"), - text=load_fixture("nws-weather-obs-valid.json"), - params={"limit": 1}, + OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json") ) aioclient_mock.get( FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") @@ -174,9 +170,7 @@ async def test_none(hass, aioclient_mock): STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") ) aioclient_mock.get( - OBSURL.format("KMIE"), - text=load_fixture("nws-weather-obs-null.json"), - params={"limit": 1}, + OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-null.json") ) aioclient_mock.get( FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-null.json") @@ -208,7 +202,6 @@ async def test_fail_obs(hass, aioclient_mock): aioclient_mock.get( OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json"), - params={"limit": 1}, status=400, ) aioclient_mock.get( @@ -234,9 +227,7 @@ async def test_fail_stn(hass, aioclient_mock): status=400, ) aioclient_mock.get( - OBSURL.format("KMIE"), - text=load_fixture("nws-weather-obs-valid.json"), - params={"limit": 1}, + OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json") ) aioclient_mock.get( FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") @@ -257,9 +248,7 @@ async def test_invalid_config(hass, aioclient_mock): STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") ) aioclient_mock.get( - OBSURL.format("KMIE"), - text=load_fixture("nws-weather-obs-valid.json"), - params={"limit": 1}, + OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json") ) aioclient_mock.get( FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") From 9d0cb899ec74744ee669b722c237b32c2f3a29a5 Mon Sep 17 00:00:00 2001 From: scheric <38077357+scheric@users.noreply.github.com> Date: Sat, 21 Sep 2019 20:07:53 +0200 Subject: [PATCH 093/296] Add optimizer data to solaredge_local (#26708) * making SENSOR_TYPES universal * bump solaredge-local version 0.2.0 * add maintenance data * add calculations for optimizer data * add new sensors * add statistics lib * update sensor data setting * making api requests universal * fix Cl * Update requirements_all.txt * fix temperature * fix f-strings * Style guidelines * shortening line length * PEP8 test * flake8 * Black test * revert line length to 80 * Repair line 12 * black * style change * Black * black using pip * fix for pylint * added proper variable name * for loop cleanup * fix capitals * Update units * black * add check for good connection to inverter * Roundig large numbers * Add myself to codeowners * Fix layout manifest * Fix layout manifest * Update manifest.json * repair manifest layout * remove newline in manifest * add myself to CODEOWNERS --- CODEOWNERS | 2 +- .../components/solaredge_local/manifest.json | 19 ++- .../components/solaredge_local/sensor.py | 139 +++++++++++++----- requirements_all.txt | 2 +- 4 files changed, 115 insertions(+), 47 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9bcd475d5d4788..700e7145838f0c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -246,7 +246,7 @@ homeassistant/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/smtp/* @fabaff -homeassistant/components/solaredge_local/* @drobtravels +homeassistant/components/solaredge_local/* @drobtravels @scheric homeassistant/components/solax/* @squishykid homeassistant/components/somfy/* @tetienne homeassistant/components/songpal/* @rytilahti diff --git a/homeassistant/components/solaredge_local/manifest.json b/homeassistant/components/solaredge_local/manifest.json index 5fb07011983edd..291c774c383d56 100644 --- a/homeassistant/components/solaredge_local/manifest.json +++ b/homeassistant/components/solaredge_local/manifest.json @@ -1,8 +1,13 @@ { - "domain": "solaredge_local", - "name": "Solar Edge Local", - "documentation": "", - "dependencies": [], - "codeowners": ["@drobtravels"], - "requirements": ["solaredge-local==0.1.4"] - } \ No newline at end of file + "domain": "solaredge_local", + "name": "Solar Edge Local", + "documentation": "https://www.home-assistant.io/components/solaredge_local", + "requirements": [ + "solaredge-local==0.2.0" + ], + "dependencies": [], + "codeowners": [ + "@drobtravels", + "@scheric" + ] +} diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 8586d950e39cdb..4fc62e44921448 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -1,19 +1,20 @@ -""" -Support for SolarEdge Monitoring API. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.solaredge_local/ -""" +"""Support for SolarEdge-local Monitoring API.""" import logging from datetime import timedelta +import statistics from requests.exceptions import HTTPError, ConnectTimeout from solaredge_local import SolarEdge import voluptuous as vol - from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME, POWER_WATT, ENERGY_WATT_HOUR +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_NAME, + POWER_WATT, + ENERGY_WATT_HOUR, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -24,9 +25,10 @@ # Supported sensor types: # Key: ['json_key', 'name', unit, icon] SENSOR_TYPES = { - "lifetime_energy": [ - "energyTotal", - "Lifetime energy", + "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"], + "energy_this_month": [ + "energyThisMonth", + "Energy this month", ENERGY_WATT_HOUR, "mdi:solar-power", ], @@ -36,19 +38,48 @@ ENERGY_WATT_HOUR, "mdi:solar-power", ], - "energy_this_month": [ - "energyThisMonth", - "Energy this month", - ENERGY_WATT_HOUR, - "mdi:solar-power", - ], "energy_today": [ "energyToday", "Energy today", ENERGY_WATT_HOUR, "mdi:solar-power", ], - "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"], + "inverter_temperature": [ + "invertertemperature", + "Inverter Temperature", + TEMP_CELSIUS, + "mdi:thermometer", + ], + "lifetime_energy": [ + "energyTotal", + "Lifetime energy", + ENERGY_WATT_HOUR, + "mdi:solar-power", + ], + "optimizer_current": [ + "optimizercurrent", + "Avrage Optimizer Current", + "A", + "mdi:solar-panel", + ], + "optimizer_power": [ + "optimizerpower", + "Avrage Optimizer Power", + POWER_WATT, + "mdi:solar-panel", + ], + "optimizer_temperature": [ + "optimizertemperature", + "Avrage Optimizer Temperature", + TEMP_CELSIUS, + "mdi:solar-panel", + ], + "optimizer_voltage": [ + "optimizervoltage", + "Avrage Optimizer Voltage", + "V", + "mdi:solar-panel", + ], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -66,18 +97,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ip_address = config[CONF_IP_ADDRESS] platform_name = config[CONF_NAME] - # Create new SolarEdge object to retrieve data + # Create new SolarEdge object to retrieve data. api = SolarEdge(f"http://{ip_address}/") - # Check if api can be reached and site is active + # Check if api can be reached and site is active. try: status = api.get_status() - - status.energy # pylint: disable=pointless-statement _LOGGER.debug("Credentials correct and site is active") except AttributeError: - _LOGGER.error("Missing details data in solaredge response") - _LOGGER.debug("Response is: %s", status) + _LOGGER.error("Missing details data in solaredge status") + _LOGGER.debug("Status is: %s", status) return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve details from SolarEdge API") @@ -111,7 +140,7 @@ def __init__(self, platform_name, sensor_key, data): @property def name(self): """Return the name.""" - return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1]) + return f"{self.platform_name} ({SENSOR_TYPES[self.sensor_key][1]})" @property def unit_of_measurement(self): @@ -147,21 +176,55 @@ def __init__(self, hass, api): def update(self): """Update the data from the SolarEdge Monitoring API.""" try: - response = self.api.get_status() - _LOGGER.debug("response from SolarEdge: %s", response) - except (ConnectTimeout): + status = self.api.get_status() + _LOGGER.debug("Status from SolarEdge: %s", status) + except ConnectTimeout: _LOGGER.error("Connection timeout, skipping update") return - except (HTTPError): - _LOGGER.error("Could not retrieve data, skipping update") + except HTTPError: + _LOGGER.error("Could not retrieve status, skipping update") return try: - self.data["energyTotal"] = response.energy.total - self.data["energyThisYear"] = response.energy.thisYear - self.data["energyThisMonth"] = response.energy.thisMonth - self.data["energyToday"] = response.energy.today - self.data["currentPower"] = response.powerWatt - _LOGGER.debug("Updated SolarEdge overview data: %s", self.data) - except AttributeError: - _LOGGER.error("Missing details data in SolarEdge response") + maintenance = self.api.get_maintenance() + _LOGGER.debug("Maintenance from SolarEdge: %s", maintenance) + except ConnectTimeout: + _LOGGER.error("Connection timeout, skipping update") + return + except HTTPError: + _LOGGER.error("Could not retrieve maintenance, skipping update") + return + + temperature = [] + voltage = [] + current = [] + power = 0 + + for optimizer in maintenance.diagnostics.inverters.primary.optimizer: + if not optimizer.online: + continue + temperature.append(optimizer.temperature.value) + voltage.append(optimizer.inputV) + current.append(optimizer.inputC) + + if not voltage: + temperature.append(0) + voltage.append(0) + current.append(0) + else: + power = statistics.mean(voltage) * statistics.mean(current) + + if status.sn: + self.data["energyTotal"] = round(status.energy.total, 2) + self.data["energyThisYear"] = round(status.energy.thisYear, 2) + self.data["energyThisMonth"] = round(status.energy.thisMonth, 2) + self.data["energyToday"] = round(status.energy.today, 2) + self.data["currentPower"] = round(status.powerWatt, 2) + self.data[ + "invertertemperature" + ] = status.inverters.primary.temperature.value + if maintenance.system.name: + self.data["optimizertemperature"] = statistics.mean(temperature) + self.data["optimizervoltage"] = statistics.mean(voltage) + self.data["optimizercurrent"] = statistics.mean(current) + self.data["optimizerpower"] = power diff --git a/requirements_all.txt b/requirements_all.txt index ef036c99c70f27..58c59a1c04c38e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1779,7 +1779,7 @@ snapcast==2.0.10 socialbladeclient==0.2 # homeassistant.components.solaredge_local -solaredge-local==0.1.4 +solaredge-local==0.2.0 # homeassistant.components.solaredge solaredge==0.0.2 From e394be73374574b11e1a7111165dbc577755b245 Mon Sep 17 00:00:00 2001 From: Albert Gouws Date: Sun, 22 Sep 2019 06:42:03 +1200 Subject: [PATCH 094/296] Mqtt binary sensor expire after (#26058) * Added expire_after to mqtt binary_sensor. Updated mqtt test_binary_sensor test. * Cleanup MQTT Binary Sensor and tests after suggestions * Updated to not alter state at all * Change to include custom expired variable, and override available property to check expired * Added # pylint: disable=no-member --- .../components/mqtt/binary_sensor.py | 46 +++++++- tests/components/mqtt/test_binary_sensor.py | 107 +++++++++++++++++- 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 4617fcf054a232..bcf398464bc7c0 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -1,4 +1,5 @@ """Support for MQTT binary sensors.""" +from datetime import timedelta import logging import voluptuous as vol @@ -21,7 +22,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.helpers.event as evt +from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import dt as dt_util from . import ( ATTR_DISCOVERY_HASH, @@ -43,12 +46,14 @@ DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_FORCE_UPDATE = False +CONF_EXPIRE_AFTER = "expire_after" PLATFORM_SCHEMA = ( mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OFF_DELAY): vol.All(vol.Coerce(int), vol.Range(min=0)), @@ -112,8 +117,9 @@ def __init__(self, config, config_entry, discovery_hash): self._unique_id = config.get(CONF_UNIQUE_ID) self._state = None self._sub_state = None + self._expiration_trigger = None self._delay_listener = None - + self._expired = None device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) @@ -153,6 +159,26 @@ def off_delay_listener(now): def state_message_received(msg): """Handle a new received MQTT state message.""" payload = msg.payload + # auto-expire enabled? + expire_after = self._config.get(CONF_EXPIRE_AFTER) + + if expire_after is not None and expire_after > 0: + + # When expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message + self._expired = False + + # Reset old trigger + if self._expiration_trigger: + self._expiration_trigger() + self._expiration_trigger = None + + # Set new trigger + expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after) + + self._expiration_trigger = async_track_point_in_utc_time( + self.hass, self.value_is_expired, expiration_at + ) + value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: payload = value_template.async_render_with_possible_json_value( @@ -202,6 +228,15 @@ async def async_will_remove_from_hass(self): await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) + @callback + def value_is_expired(self, *_): + """Triggered when value is expired.""" + + self._expiration_trigger = None + self._expired = True + + self.async_write_ha_state() + @property def should_poll(self): """Return the polling state.""" @@ -231,3 +266,12 @@ def force_update(self): def unique_id(self): """Return a unique ID.""" return self._unique_id + + @property + def available(self) -> bool: + """Return true if the device is available and value has not expired.""" + expire_after = self._config.get(CONF_EXPIRE_AFTER) + # pylint: disable=no-member + return MqttAvailability.available.fget(self) and ( + expire_after is None or not self._expired + ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index af27ff8c7d1c3d..28f1a7e9720091 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1,7 +1,8 @@ """The tests for the MQTT binary sensor platform.""" -from datetime import timedelta +from datetime import datetime, timedelta import json -from unittest.mock import ANY + +from unittest.mock import ANY, patch from homeassistant.components import binary_sensor, mqtt from homeassistant.components.mqtt.discovery import async_start @@ -24,6 +25,107 @@ ) +async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog): + """Test the expiration of the value.""" + assert await async_setup_component( + hass, + binary_sensor.DOMAIN, + { + binary_sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "expire_after": 4, + "force_update": True, + "availability_topic": "availability-topic", + } + }, + ) + + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic", "online") + + state = hass.states.get("binary_sensor.test") + assert state.state != STATE_UNAVAILABLE + + await expires_helper(hass, mqtt_mock, caplog) + + +async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): + """Test the expiration of the value.""" + assert await async_setup_component( + hass, + binary_sensor.DOMAIN, + { + binary_sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "expire_after": 4, + "force_update": True, + } + }, + ) + + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF + + await expires_helper(hass, mqtt_mock, caplog) + + +async def expires_helper(hass, mqtt_mock, caplog): + """Run the basic expiry code.""" + + now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) + with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now): + async_fire_time_changed(hass, now) + async_fire_mqtt_message(hass, "test-topic", "ON") + await hass.async_block_till_done() + + # Value was set correctly. + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_ON + + # Time jump +3s + now = now + timedelta(seconds=3) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Value is not yet expired + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_ON + + # Next message resets timer + with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now): + async_fire_time_changed(hass, now) + async_fire_mqtt_message(hass, "test-topic", "OFF") + await hass.async_block_till_done() + + # Value was updated correctly. + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF + + # Time jump +3s + now = now + timedelta(seconds=3) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Value is not yet expired + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF + + # Time jump +2s + now = now + timedelta(seconds=2) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Value is expired now + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_UNAVAILABLE + + async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -41,6 +143,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): ) state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF async_fire_mqtt_message(hass, "test-topic", "ON") From 8c580209a65c288bdc7716887e87f0f026933c1e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 21 Sep 2019 20:52:35 +0200 Subject: [PATCH 095/296] Upgrade importlib-metadata to 0.23 (#26787) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 900bfddda2ec10..32804d790411f2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 home-assistant-frontend==20190919.0 -importlib-metadata==0.19 +importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 58c59a1c04c38e..8af0da567da609 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ attrs==19.1.0 bcrypt==3.1.7 certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" -importlib-metadata==0.19 +importlib-metadata==0.23 jinja2>=2.10.1 PyJWT==1.7.1 cryptography==2.7 diff --git a/setup.py b/setup.py index e6776d8a1a0811..87704a3c6a96b4 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ "bcrypt==3.1.7", "certifi>=2019.6.16", 'contextvars==2.4;python_version<"3.7"', - "importlib-metadata==0.19", + "importlib-metadata==0.23", "jinja2>=2.10.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. From a9ff15077c8e8ea4fef7b8bdefe543bc59b1a467 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 21 Sep 2019 20:52:46 +0200 Subject: [PATCH 096/296] Upgrade python-whois to 0.7.2 (#26788) --- homeassistant/components/whois/manifest.json | 2 +- homeassistant/components/whois/sensor.py | 11 ++++------- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json index dec3e78a50362b..6040c8655b9f25 100644 --- a/homeassistant/components/whois/manifest.json +++ b/homeassistant/components/whois/manifest.json @@ -3,7 +3,7 @@ "name": "Whois", "documentation": "https://www.home-assistant.io/components/whois", "requirements": [ - "python-whois==0.7.1" + "python-whois==0.7.2" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 313a6337a11b5b..09cf40f193fe87 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -3,6 +3,7 @@ import logging import voluptuous as vol +import whois from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME @@ -32,8 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the WHOIS sensor.""" - import whois - domain = config.get(CONF_DOMAIN) name = config.get(CONF_NAME) @@ -41,7 +40,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if "expiration_date" in whois.whois(domain): add_entities([WhoisSensor(name, domain)], True) else: - _LOGGER.error("WHOIS lookup for %s didn't contain expiration_date", domain) + _LOGGER.error( + "WHOIS lookup for %s didn't contain an expiration date", domain + ) return except whois.BaseException as ex: _LOGGER.error("Exception %s occurred during WHOIS lookup for %s", ex, domain) @@ -53,8 +54,6 @@ class WhoisSensor(Entity): def __init__(self, name, domain): """Initialize the sensor.""" - import whois - self.whois = whois.whois self._name = name @@ -95,8 +94,6 @@ def _empty_state_and_attributes(self): def update(self): """Get the current WHOIS data for the domain.""" - import whois - try: response = self.whois(self._domain) except whois.BaseException as ex: diff --git a/requirements_all.txt b/requirements_all.txt index 8af0da567da609..c67928093c37cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1569,7 +1569,7 @@ python-velbus==2.0.27 python-vlc==1.1.2 # homeassistant.components.whois -python-whois==0.7.1 +python-whois==0.7.2 # homeassistant.components.wink python-wink==1.10.5 From 9e79920c9c65bc343ccb3a15fa59588c1eba304c Mon Sep 17 00:00:00 2001 From: Zach Date: Sat, 21 Sep 2019 14:53:19 -0400 Subject: [PATCH 097/296] Fix doods missing detector name kwarg (#26784) * Fix missing detector name kwarg * Updated requirements_all.txt * Reformatted --- homeassistant/components/doods/image_processing.py | 5 ++++- homeassistant/components/doods/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index ba44d86c2e4083..850eae76040f2a 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -139,6 +139,7 @@ def __init__(self, hass, camera_entity, name, doods, detector, config): self._name = f"Doods {name}" self._doods = doods self._file_out = config[CONF_FILE_OUT] + self._detector_name = detector["name"] # detector config and aspect ratio self._width = None @@ -289,7 +290,9 @@ def process_image(self, image): # Run detection start = time.time() - response = self._doods.detect(image, self._dconfig) + response = self._doods.detect( + image, dconfig=self._dconfig, detector_name=self._detector_name + ) _LOGGER.debug( "doods detect: %s response: %s duration: %s", self._dconfig, diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 3e1ce22a230b03..75c1bd3dcd3814 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -3,8 +3,8 @@ "name": "DOODS - Distributed Outside Object Detection Service", "documentation": "https://www.home-assistant.io/components/doods", "requirements": [ - "pydoods==1.0.1" + "pydoods==1.0.2" ], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index c67928093c37cf..a17c201a9528d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,7 +1140,7 @@ pydelijn==0.5.1 pydispatcher==2.0.5 # homeassistant.components.doods -pydoods==1.0.1 +pydoods==1.0.2 # homeassistant.components.android_ip_webcam pydroid-ipcam==0.8 From 88dcecab397c69a3e2257cc1cb4116367a8b2d8f Mon Sep 17 00:00:00 2001 From: John Luetke Date: Sat, 21 Sep 2019 12:39:49 -0700 Subject: [PATCH 098/296] Add myself as a pi_hole codeowner (#26796) --- CODEOWNERS | 2 +- homeassistant/components/pi_hole/manifest.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 700e7145838f0c..abd3379221a9a8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -207,7 +207,7 @@ homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus -homeassistant/components/pi_hole/* @fabaff +homeassistant/components/pi_hole/* @fabaff @johnluetke homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index 2d19ab25fe78dd..7fe8bba6873913 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -7,6 +7,7 @@ ], "dependencies": [], "codeowners": [ - "@fabaff" + "@fabaff", + "@johnluetke" ] } From dc52b858a40905c129684a8e964ce182c4ff00df Mon Sep 17 00:00:00 2001 From: bouni Date: Sun, 22 Sep 2019 01:22:33 +0200 Subject: [PATCH 099/296] Fix spaceapi (#26453) * fixed latitude/longitude keys to be conform with spaceapi specification * version is now a string as required by the spaceapi specification * add spacefed * fixed lat/lon in spaceapi tests * extended tests * add feeds * extended tests * add cache * add more tests * add projects * more tests * add radio_show * more tests * add additional contact attributes * corrected valid issue_repoer_channel options * validate min length of contact/keymasters * fixed location as address is not required by spec * Update homeassistant/components/spaceapi/__init__.py Co-Authored-By: Fabian Affolter * Update homeassistant/components/spaceapi/__init__.py Co-Authored-By: Fabian Affolter * fixed issue with name change for longitude/latitude --- homeassistant/components/spaceapi/__init__.py | 187 ++++++++++++++++-- tests/components/spaceapi/test_init.py | 58 +++++- 2 files changed, 229 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/spaceapi/__init__.py b/homeassistant/components/spaceapi/__init__.py index 607d9c45538142..ea5a64d97e7cf2 100644 --- a/homeassistant/components/spaceapi/__init__.py +++ b/homeassistant/components/spaceapi/__init__.py @@ -7,9 +7,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, - ATTR_LATITUDE, ATTR_LOCATION, - ATTR_LONGITUDE, ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, CONF_ADDRESS, @@ -26,6 +24,15 @@ _LOGGER = logging.getLogger(__name__) ATTR_ADDRESS = "address" +ATTR_SPACEFED = "spacefed" +ATTR_CAM = "cam" +ATTR_STREAM = "stream" +ATTR_FEEDS = "feeds" +ATTR_CACHE = "cache" +ATTR_PROJECTS = "projects" +ATTR_RADIO_SHOW = "radio_show" +ATTR_LAT = "lat" +ATTR_LON = "lon" ATTR_API = "api" ATTR_CLOSE = "close" ATTR_CONTACT = "contact" @@ -49,32 +56,135 @@ CONF_IRC = "irc" CONF_ISSUE_REPORT_CHANNELS = "issue_report_channels" CONF_LOCATION = "location" +CONF_SPACEFED = "spacefed" +CONF_SPACENET = "spacenet" +CONF_SPACESAML = "spacesaml" +CONF_SPACEPHONE = "spacephone" +CONF_CAM = "cam" +CONF_STREAM = "stream" +CONF_M4 = "m4" +CONF_MJPEG = "mjpeg" +CONF_USTREAM = "ustream" +CONF_FEEDS = "feeds" +CONF_FEED_BLOG = "blog" +CONF_FEED_WIKI = "wiki" +CONF_FEED_CALENDAR = "calendar" +CONF_FEED_FLICKER = "flicker" +CONF_FEED_TYPE = "type" +CONF_FEED_URL = "url" +CONF_CACHE = "cache" +CONF_CACHE_SCHEDULE = "schedule" +CONF_PROJECTS = "projects" +CONF_RADIO_SHOW = "radio_show" +CONF_RADIO_SHOW_NAME = "name" +CONF_RADIO_SHOW_URL = "url" +CONF_RADIO_SHOW_TYPE = "type" +CONF_RADIO_SHOW_START = "start" +CONF_RADIO_SHOW_END = "end" CONF_LOGO = "logo" -CONF_MAILING_LIST = "mailing_list" CONF_PHONE = "phone" +CONF_SIP = "sip" +CONF_KEYMASTERS = "keymasters" +CONF_KEYMASTER_NAME = "name" +CONF_KEYMASTER_IRC_NICK = "irc_nick" +CONF_KEYMASTER_PHONE = "phone" +CONF_KEYMASTER_EMAIL = "email" +CONF_KEYMASTER_TWITTER = "twitter" +CONF_TWITTER = "twitter" +CONF_FACEBOOK = "facebook" +CONF_IDENTICA = "identica" +CONF_FOURSQUARE = "foursquare" +CONF_ML = "ml" +CONF_JABBER = "jabber" +CONF_ISSUE_MAIL = "issue_mail" CONF_SPACE = "space" CONF_TEMPERATURE = "temperature" -CONF_TWITTER = "twitter" DATA_SPACEAPI = "data_spaceapi" DOMAIN = "spaceapi" -ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_IRC, CONF_MAILING_LIST, CONF_TWITTER] +ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_ISSUE_MAIL, CONF_ML, CONF_TWITTER] SENSOR_TYPES = [CONF_HUMIDITY, CONF_TEMPERATURE] -SPACEAPI_VERSION = 0.13 +SPACEAPI_VERSION = "0.13" URL_API_SPACEAPI = "/api/spaceapi" -LOCATION_SCHEMA = vol.Schema({vol.Optional(CONF_ADDRESS): cv.string}, required=True) +LOCATION_SCHEMA = vol.Schema({vol.Optional(CONF_ADDRESS): cv.string}) + +SPACEFED_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SPACENET): cv.boolean, + vol.Optional(CONF_SPACESAML): cv.boolean, + vol.Optional(CONF_SPACEPHONE): cv.boolean, + } +) + +STREAM_SCHEMA = vol.Schema( + { + vol.Optional(CONF_M4): cv.url, + vol.Optional(CONF_MJPEG): cv.url, + vol.Optional(CONF_USTREAM): cv.url, + } +) + +FEED_SCHEMA = vol.Schema( + {vol.Optional(CONF_FEED_TYPE): cv.string, vol.Required(CONF_FEED_URL): cv.url} +) + +FEEDS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_FEED_BLOG): FEED_SCHEMA, + vol.Optional(CONF_FEED_WIKI): FEED_SCHEMA, + vol.Optional(CONF_FEED_CALENDAR): FEED_SCHEMA, + vol.Optional(CONF_FEED_FLICKER): FEED_SCHEMA, + } +) + +CACHE_SCHEMA = vol.Schema( + { + vol.Required(CONF_CACHE_SCHEDULE): cv.matches_regex( + r"(m.02|m.05|m.10|m.15|m.30|h.01|h.02|h.04|h.08|h.12|d.01)" + ) + } +) + +RADIO_SHOW_SCHEMA = vol.Schema( + { + vol.Required(CONF_RADIO_SHOW_NAME): cv.string, + vol.Required(CONF_RADIO_SHOW_URL): cv.url, + vol.Required(CONF_RADIO_SHOW_TYPE): cv.matches_regex(r"(mp3|ogg)"), + vol.Required(CONF_RADIO_SHOW_START): cv.string, + vol.Required(CONF_RADIO_SHOW_END): cv.string, + } +) + +KEYMASTER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_KEYMASTER_NAME): cv.string, + vol.Optional(CONF_KEYMASTER_IRC_NICK): cv.string, + vol.Optional(CONF_KEYMASTER_PHONE): cv.string, + vol.Optional(CONF_KEYMASTER_EMAIL): cv.string, + vol.Optional(CONF_KEYMASTER_TWITTER): cv.string, + } +) CONTACT_SCHEMA = vol.Schema( { vol.Optional(CONF_EMAIL): cv.string, vol.Optional(CONF_IRC): cv.string, - vol.Optional(CONF_MAILING_LIST): cv.string, + vol.Optional(CONF_ML): cv.string, vol.Optional(CONF_PHONE): cv.string, vol.Optional(CONF_TWITTER): cv.string, + vol.Optional(CONF_SIP): cv.string, + vol.Optional(CONF_FACEBOOK): cv.string, + vol.Optional(CONF_IDENTICA): cv.string, + vol.Optional(CONF_FOURSQUARE): cv.string, + vol.Optional(CONF_JABBER): cv.string, + vol.Optional(CONF_ISSUE_MAIL): cv.string, + vol.Optional(CONF_KEYMASTERS): vol.All( + cv.ensure_list, [KEYMASTER_SCHEMA], vol.Length(min=1) + ), }, required=False, ) @@ -100,12 +210,23 @@ vol.Required(CONF_ISSUE_REPORT_CHANNELS): vol.All( cv.ensure_list, [vol.In(ISSUE_REPORT_CHANNELS)] ), - vol.Required(CONF_LOCATION): LOCATION_SCHEMA, + vol.Optional(CONF_LOCATION): LOCATION_SCHEMA, vol.Required(CONF_LOGO): cv.url, vol.Required(CONF_SPACE): cv.string, vol.Required(CONF_STATE): STATE_SCHEMA, vol.Required(CONF_URL): cv.string, vol.Optional(CONF_SENSORS): SENSOR_SCHEMA, + vol.Optional(CONF_SPACEFED): SPACEFED_SCHEMA, + vol.Optional(CONF_CAM): vol.All( + cv.ensure_list, [cv.url], vol.Length(min=1) + ), + vol.Optional(CONF_STREAM): STREAM_SCHEMA, + vol.Optional(CONF_FEEDS): FEEDS_SCHEMA, + vol.Optional(CONF_CACHE): CACHE_SCHEMA, + vol.Optional(CONF_PROJECTS): vol.All(cv.ensure_list, [cv.url]), + vol.Optional(CONF_RADIO_SHOW): vol.All( + cv.ensure_list, [RADIO_SHOW_SCHEMA] + ), } ) }, @@ -150,11 +271,14 @@ def get(self, request): spaceapi = dict(hass.data[DATA_SPACEAPI]) is_sensors = spaceapi.get("sensors") - location = { - ATTR_ADDRESS: spaceapi[ATTR_LOCATION][CONF_ADDRESS], - ATTR_LATITUDE: hass.config.latitude, - ATTR_LONGITUDE: hass.config.longitude, - } + location = {ATTR_LAT: hass.config.latitude, ATTR_LON: hass.config.longitude} + + try: + location[ATTR_ADDRESS] = spaceapi[ATTR_LOCATION][CONF_ADDRESS] + except KeyError: + pass + except TypeError: + pass state_entity = spaceapi["state"][ATTR_ENTITY_ID] space_state = hass.states.get(state_entity) @@ -186,6 +310,41 @@ def get(self, request): ATTR_URL: spaceapi[CONF_URL], } + try: + data[ATTR_CAM] = spaceapi[CONF_CAM] + except KeyError: + pass + + try: + data[ATTR_SPACEFED] = spaceapi[CONF_SPACEFED] + except KeyError: + pass + + try: + data[ATTR_STREAM] = spaceapi[CONF_STREAM] + except KeyError: + pass + + try: + data[ATTR_FEEDS] = spaceapi[CONF_FEEDS] + except KeyError: + pass + + try: + data[ATTR_CACHE] = spaceapi[CONF_CACHE] + except KeyError: + pass + + try: + data[ATTR_PROJECTS] = spaceapi[CONF_PROJECTS] + except KeyError: + pass + + try: + data[ATTR_RADIO_SHOW] = spaceapi[CONF_RADIO_SHOW] + except KeyError: + pass + if is_sensors is not None: sensors = {} for sensor_type in is_sensors: diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py index 02a6eccc285804..58c417831a966f 100644 --- a/tests/components/spaceapi/test_init.py +++ b/tests/components/spaceapi/test_init.py @@ -25,6 +25,34 @@ "temperature": ["test.temp1", "test.temp2"], "humidity": ["test.hum1"], }, + "spacefed": {"spacenet": True, "spacesaml": False, "spacephone": True}, + "cam": ["https://home-assistant.io/cam1", "https://home-assistant.io/cam2"], + "stream": { + "m4": "https://home-assistant.io/m4", + "mjpeg": "https://home-assistant.io/mjpeg", + "ustream": "https://home-assistant.io/ustream", + }, + "feeds": { + "blog": {"url": "https://home-assistant.io/blog"}, + "wiki": {"type": "mediawiki", "url": "https://home-assistant.io/wiki"}, + "calendar": {"type": "ical", "url": "https://home-assistant.io/calendar"}, + "flicker": {"url": "https://www.flickr.com/photos/home-assistant"}, + }, + "cache": {"schedule": "m.02"}, + "projects": [ + "https://home-assistant.io/projects/1", + "https://home-assistant.io/projects/2", + "https://home-assistant.io/projects/3", + ], + "radio_show": [ + { + "name": "Radioshow", + "url": "https://home-assistant.io/radio", + "type": "ogg", + "start": "2019-09-02T10:00Z", + "end": "2019-09-02T12:00Z", + } + ], } } @@ -61,11 +89,37 @@ async def test_spaceapi_get(hass, mock_client): assert data["space"] == "Home" assert data["contact"]["email"] == "hello@home-assistant.io" assert data["location"]["address"] == "In your Home" - assert data["location"]["latitude"] == 32.87336 - assert data["location"]["longitude"] == -117.22743 + assert data["location"]["lat"] == 32.87336 + assert data["location"]["lon"] == -117.22743 assert data["state"]["open"] == "null" assert data["state"]["icon"]["open"] == "https://home-assistant.io/open.png" assert data["state"]["icon"]["close"] == "https://home-assistant.io/close.png" + assert data["spacefed"]["spacenet"] == bool(1) + assert data["spacefed"]["spacesaml"] == bool(0) + assert data["spacefed"]["spacephone"] == bool(1) + assert data["cam"][0] == "https://home-assistant.io/cam1" + assert data["cam"][1] == "https://home-assistant.io/cam2" + assert data["stream"]["m4"] == "https://home-assistant.io/m4" + assert data["stream"]["mjpeg"] == "https://home-assistant.io/mjpeg" + assert data["stream"]["ustream"] == "https://home-assistant.io/ustream" + assert data["feeds"]["blog"]["url"] == "https://home-assistant.io/blog" + assert data["feeds"]["wiki"]["type"] == "mediawiki" + assert data["feeds"]["wiki"]["url"] == "https://home-assistant.io/wiki" + assert data["feeds"]["calendar"]["type"] == "ical" + assert data["feeds"]["calendar"]["url"] == "https://home-assistant.io/calendar" + assert ( + data["feeds"]["flicker"]["url"] + == "https://www.flickr.com/photos/home-assistant" + ) + assert data["cache"]["schedule"] == "m.02" + assert data["projects"][0] == "https://home-assistant.io/projects/1" + assert data["projects"][1] == "https://home-assistant.io/projects/2" + assert data["projects"][2] == "https://home-assistant.io/projects/3" + assert data["radio_show"][0]["name"] == "Radioshow" + assert data["radio_show"][0]["url"] == "https://home-assistant.io/radio" + assert data["radio_show"][0]["type"] == "ogg" + assert data["radio_show"][0]["start"] == "2019-09-02T10:00Z" + assert data["radio_show"][0]["end"] == "2019-09-02T12:00Z" async def test_spaceapi_state_get(hass, mock_client): From 544cdae67c3acce9105b2d334fa924beedb52b80 Mon Sep 17 00:00:00 2001 From: CQoute Date: Sun, 22 Sep 2019 08:59:52 +0930 Subject: [PATCH 100/296] Update light.py (#26703) Fix for esphome lights to use the flash feature --- homeassistant/components/esphome/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index e455d5581d1e3d..1205521706eb21 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -74,7 +74,7 @@ async def async_turn_on(self, **kwargs) -> None: red, green, blue = color_util.color_hsv_to_RGB(hue, sat, 100) data["rgb"] = (red / 255, green / 255, blue / 255) if ATTR_FLASH in kwargs: - data["flash"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] + data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] if ATTR_TRANSITION in kwargs: data["transition_length"] = kwargs[ATTR_TRANSITION] if ATTR_BRIGHTNESS in kwargs: From 6135b862ba697b2f4967982234423ca3354d3adb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 07:10:21 +0200 Subject: [PATCH 101/296] Bump hbmqtt to 0.9.5 (#26804) --- homeassistant/components/mqtt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index d63d1707fac2eb..2df50699a9d8d3 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/mqtt", "requirements": [ - "hbmqtt==0.9.4", + "hbmqtt==0.9.5", "paho-mqtt==1.4.0" ], "dependencies": [ diff --git a/requirements_all.txt b/requirements_all.txt index a17c201a9528d6..b4ea5b509d34d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -612,7 +612,7 @@ hangups==0.4.9 hass-nabucasa==0.17 # homeassistant.components.mqtt -hbmqtt==0.9.4 +hbmqtt==0.9.5 # homeassistant.components.jewish_calendar hdate==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a8aa92c81ddbbc..214f2ee30ed1d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -166,7 +166,7 @@ hangups==0.4.9 hass-nabucasa==0.17 # homeassistant.components.mqtt -hbmqtt==0.9.4 +hbmqtt==0.9.5 # homeassistant.components.jewish_calendar hdate==0.9.0 From ef0dd689fab68ca16fb9edded37353f39470e20e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 07:10:34 +0200 Subject: [PATCH 102/296] Bump python-slugify to 3.0.4 (#26801) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 32804d790411f2..842cf4840c832a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 -python-slugify==3.0.3 +python-slugify==3.0.4 pytz>=2019.02 pyyaml==5.1.2 requests==2.22.0 diff --git a/requirements_all.txt b/requirements_all.txt index b4ea5b509d34d0..42082b76ae0604 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -11,7 +11,7 @@ jinja2>=2.10.1 PyJWT==1.7.1 cryptography==2.7 pip>=8.0.3 -python-slugify==3.0.3 +python-slugify==3.0.4 pytz>=2019.02 pyyaml==5.1.2 requests==2.22.0 diff --git a/setup.py b/setup.py index 87704a3c6a96b4..26f112bb008207 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ # PyJWT has loose dependency. We want the latest one. "cryptography==2.7", "pip>=8.0.3", - "python-slugify==3.0.3", + "python-slugify==3.0.4", "pytz>=2019.02", "pyyaml==5.1.2", "requests==2.22.0", From 6bdfab1124d9d2494069e9df6d24be980db07f56 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 09:56:43 +0200 Subject: [PATCH 103/296] Bump pytest to 5.1.3 (#26794) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 44b27d8e13e9c5..e697164a35a24b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -17,5 +17,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.1.2 +pytest==5.1.3 requests_mock==1.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 214f2ee30ed1d4..e3df0099032178 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -18,7 +18,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.1.2 +pytest==5.1.3 requests_mock==1.6.0 From a5ebf9f38d492e9b226bf2df40df2c70caf2adeb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 09:57:02 +0200 Subject: [PATCH 104/296] Bump iperf3 to 0.1.11 (#26795) --- homeassistant/components/iperf3/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json index e35be24fc8089f..0547628b4bfd2c 100644 --- a/homeassistant/components/iperf3/manifest.json +++ b/homeassistant/components/iperf3/manifest.json @@ -3,7 +3,7 @@ "name": "Iperf3", "documentation": "https://www.home-assistant.io/components/iperf3", "requirements": [ - "iperf3==0.1.10" + "iperf3==0.1.11" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 42082b76ae0604..1e2c466a754ac5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -693,7 +693,7 @@ influxdb==5.2.3 insteonplm==0.16.5 # homeassistant.components.iperf3 -iperf3==0.1.10 +iperf3==0.1.11 # homeassistant.components.route53 ipify==1.0.0 From 48369ad08ff996fd6821ca527f4da188e6755ac6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 09:57:11 +0200 Subject: [PATCH 105/296] Bump shodan to 1.17.0 (#26797) * Bump shodan to 1.16.0 * Bump shodan to 1.17.0 --- homeassistant/components/shodan/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 7ecc298e3f6f17..be7f0a524dcd0b 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -3,7 +3,7 @@ "name": "Shodan", "documentation": "https://www.home-assistant.io/components/shodan", "requirements": [ - "shodan==1.15.0" + "shodan==1.17.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 1e2c466a754ac5..4baf0de152ed5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1732,7 +1732,7 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.15.0 +shodan==1.17.0 # homeassistant.components.simplepush simplepush==1.1.4 From 4f7a4b93da91d2792a8ac8600b23571f06dba15b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 10:02:23 +0200 Subject: [PATCH 106/296] Bump request_mock to 1.7.0 (#26799) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index e697164a35a24b..b9b919c4bfd077 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -18,4 +18,4 @@ pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==5.1.3 -requests_mock==1.6.0 +requests_mock==1.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3df0099032178..6b4d7fbc089a0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==5.1.3 -requests_mock==1.6.0 +requests_mock==1.7.0 # homeassistant.components.homekit From 04cae0818dae9a22d76deed483b73af8c85a3403 Mon Sep 17 00:00:00 2001 From: Dima Zavin Date: Sun, 22 Sep 2019 02:42:00 -0700 Subject: [PATCH 107/296] Bump pylutron to 0.2.5 (#26815) --- homeassistant/components/lutron/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index bece55ae09d819..451a6f3e33d822 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -3,7 +3,7 @@ "name": "Lutron", "documentation": "https://www.home-assistant.io/components/lutron", "requirements": [ - "pylutron==0.2.2" + "pylutron==0.2.5" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 4baf0de152ed5c..08ed0d4476f143 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1288,7 +1288,7 @@ pyloopenergy==0.1.3 pylutron-caseta==0.5.0 # homeassistant.components.lutron -pylutron==0.2.2 +pylutron==0.2.5 # homeassistant.components.mailgun pymailgunner==1.4 From 5624e3efd47046a8fe773d7def0caf439fd04003 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 22 Sep 2019 11:43:41 +0200 Subject: [PATCH 108/296] Upgrade sendgrid to 6.1.0 (#26809) * Upgrade sendgrid to 6.1.0 * Move import (could to be a Black issue) --- homeassistant/components/sendgrid/manifest.json | 2 +- homeassistant/components/sendgrid/notify.py | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index eb006f408bcbd8..1ffbe69888f329 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -3,7 +3,7 @@ "name": "Sendgrid", "documentation": "https://www.home-assistant.io/components/sendgrid", "requirements": [ - "sendgrid==6.0.5" + "sendgrid==6.1.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py index ac334587b89035..f16758a53559c2 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -3,6 +3,8 @@ import voluptuous as vol +from sendgrid import SendGridAPIClient + from homeassistant.const import ( CONF_API_KEY, CONF_RECIPIENT, @@ -45,8 +47,6 @@ class SendgridNotificationService(BaseNotificationService): def __init__(self, config): """Initialize the service.""" - from sendgrid import SendGridAPIClient - self.api_key = config[CONF_API_KEY] self.sender = config[CONF_SENDER] self.sender_name = config[CONF_SENDER_NAME] diff --git a/requirements_all.txt b/requirements_all.txt index 08ed0d4476f143..ad7ba89e4ca618 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1720,7 +1720,7 @@ schiene==0.23 scsgate==0.1.0 # homeassistant.components.sendgrid -sendgrid==6.0.5 +sendgrid==6.1.0 # homeassistant.components.sensehat sense-hat==2.2.0 From 14647f539138b88751d56a38e7eb813fae5b9fb6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 17:31:01 +0200 Subject: [PATCH 109/296] Exempt 'Help wanted' issue from stale bot (#26829) --- .github/stale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/stale.yml b/.github/stale.yml index a1a35e9f3b1ada..44cd95e1f5d710 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -13,6 +13,7 @@ onlyLabels: [] # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - under investigation + - Help wanted # Set to true to ignore issues in a project (defaults to false) exemptProjects: true From e5f6f33340a4ef13e5b2de9b94c791a18ce7d5a9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 22 Sep 2019 20:13:17 +0200 Subject: [PATCH 110/296] Add device automation support to binary_sensor entities (#26643) * Add device automation support to binary_sensor entities * turn_on -> turned_on * Correct spelling of present * Improve tests * Fix strings * Fix stale comment --- .../binary_sensor/device_automation.py | 423 ++++++++++++++++++ .../components/binary_sensor/strings.json | 93 ++++ .../binary_sensor/test_device_automation.py | 309 +++++++++++++ .../custom_components/test/binary_sensor.py | 50 +++ 4 files changed, 875 insertions(+) create mode 100644 homeassistant/components/binary_sensor/device_automation.py create mode 100644 homeassistant/components/binary_sensor/strings.json create mode 100644 tests/components/binary_sensor/test_device_automation.py create mode 100644 tests/testing_config/custom_components/test/binary_sensor.py diff --git a/homeassistant/components/binary_sensor/device_automation.py b/homeassistant/components/binary_sensor/device_automation.py new file mode 100644 index 00000000000000..c609c2eb5da4c8 --- /dev/null +++ b/homeassistant/components/binary_sensor/device_automation.py @@ -0,0 +1,423 @@ +"""Provides device automations for lights.""" +import voluptuous as vol + +import homeassistant.components.automation.state as state +from homeassistant.components.device_automation.const import ( + CONF_IS_OFF, + CONF_IS_ON, + CONF_TURNED_OFF, + CONF_TURNED_ON, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import split_entity_id +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import condition, config_validation as cv + +from . import ( + DOMAIN, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, +) + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_IS_BAT_LOW = "is_bat_low" +CONF_IS_NOT_BAT_LOW = "is_not_bat_low" +CONF_IS_COLD = "is_cold" +CONF_IS_NOT_COLD = "is_not_cold" +CONF_IS_CONNECTED = "is_connected" +CONF_IS_NOT_CONNECTED = "is_not_connected" +CONF_IS_GAS = "is_gas" +CONF_IS_NO_GAS = "is_no_gas" +CONF_IS_HOT = "is_hot" +CONF_IS_NOT_HOT = "is_not_hot" +CONF_IS_LIGHT = "is_light" +CONF_IS_NO_LIGHT = "is_no_light" +CONF_IS_LOCKED = "is_locked" +CONF_IS_NOT_LOCKED = "is_not_locked" +CONF_IS_MOIST = "is_moist" +CONF_IS_NOT_MOIST = "is_not_moist" +CONF_IS_MOTION = "is_motion" +CONF_IS_NO_MOTION = "is_no_motion" +CONF_IS_MOVING = "is_moving" +CONF_IS_NOT_MOVING = "is_not_moving" +CONF_IS_OCCUPIED = "is_occupied" +CONF_IS_NOT_OCCUPIED = "is_not_occupied" +CONF_IS_PLUGGED_IN = "is_plugged_in" +CONF_IS_NOT_PLUGGED_IN = "is_not_plugged_in" +CONF_IS_POWERED = "is_powered" +CONF_IS_NOT_POWERED = "is_not_powered" +CONF_IS_PRESENT = "is_present" +CONF_IS_NOT_PRESENT = "is_not_present" +CONF_IS_PROBLEM = "is_problem" +CONF_IS_NO_PROBLEM = "is_no_problem" +CONF_IS_UNSAFE = "is_unsafe" +CONF_IS_NOT_UNSAFE = "is_not_unsafe" +CONF_IS_SMOKE = "is_smoke" +CONF_IS_NO_SMOKE = "is_no_smoke" +CONF_IS_SOUND = "is_sound" +CONF_IS_NO_SOUND = "is_no_sound" +CONF_IS_VIBRATION = "is_vibration" +CONF_IS_NO_VIBRATION = "is_no_vibration" +CONF_IS_OPEN = "is_open" +CONF_IS_NOT_OPEN = "is_not_open" + +CONF_BAT_LOW = "bat_low" +CONF_NOT_BAT_LOW = "not_bat_low" +CONF_COLD = "cold" +CONF_NOT_COLD = "not_cold" +CONF_CONNECTED = "connected" +CONF_NOT_CONNECTED = "not_connected" +CONF_GAS = "gas" +CONF_NO_GAS = "no_gas" +CONF_HOT = "hot" +CONF_NOT_HOT = "not_hot" +CONF_LIGHT = "light" +CONF_NO_LIGHT = "no_light" +CONF_LOCKED = "locked" +CONF_NOT_LOCKED = "not_locked" +CONF_MOIST = "moist" +CONF_NOT_MOIST = "not_moist" +CONF_MOTION = "motion" +CONF_NO_MOTION = "no_motion" +CONF_MOVING = "moving" +CONF_NOT_MOVING = "not_moving" +CONF_OCCUPIED = "occupied" +CONF_NOT_OCCUPIED = "not_occupied" +CONF_PLUGGED_IN = "plugged_in" +CONF_NOT_PLUGGED_IN = "not_plugged_in" +CONF_POWERED = "powered" +CONF_NOT_POWERED = "not_powered" +CONF_PRESENT = "present" +CONF_NOT_PRESENT = "not_present" +CONF_PROBLEM = "problem" +CONF_NO_PROBLEM = "no_problem" +CONF_UNSAFE = "unsafe" +CONF_NOT_UNSAFE = "not_unsafe" +CONF_SMOKE = "smoke" +CONF_NO_SMOKE = "no_smoke" +CONF_SOUND = "sound" +CONF_NO_SOUND = "no_sound" +CONF_VIBRATION = "vibration" +CONF_NO_VIBRATION = "no_vibration" +CONF_OPEN = "open" +CONF_NOT_OPEN = "not_open" + +IS_ON = [ + CONF_IS_BAT_LOW, + CONF_IS_COLD, + CONF_IS_CONNECTED, + CONF_IS_GAS, + CONF_IS_HOT, + CONF_IS_LIGHT, + CONF_IS_LOCKED, + CONF_IS_MOIST, + CONF_IS_MOTION, + CONF_IS_MOVING, + CONF_IS_OCCUPIED, + CONF_IS_OPEN, + CONF_IS_PLUGGED_IN, + CONF_IS_POWERED, + CONF_IS_PRESENT, + CONF_IS_PROBLEM, + CONF_IS_SMOKE, + CONF_IS_SOUND, + CONF_IS_UNSAFE, + CONF_IS_VIBRATION, + CONF_IS_ON, +] + +IS_OFF = [ + CONF_IS_NOT_BAT_LOW, + CONF_IS_NOT_COLD, + CONF_IS_NOT_CONNECTED, + CONF_IS_NOT_HOT, + CONF_IS_NOT_LOCKED, + CONF_IS_NOT_MOIST, + CONF_IS_NOT_MOVING, + CONF_IS_NOT_OCCUPIED, + CONF_IS_NOT_OPEN, + CONF_IS_NOT_PLUGGED_IN, + CONF_IS_NOT_POWERED, + CONF_IS_NOT_PRESENT, + CONF_IS_NOT_UNSAFE, + CONF_IS_NO_GAS, + CONF_IS_NO_LIGHT, + CONF_IS_NO_MOTION, + CONF_IS_NO_PROBLEM, + CONF_IS_NO_SMOKE, + CONF_IS_NO_SOUND, + CONF_IS_NO_VIBRATION, + CONF_IS_OFF, +] + +TURNED_ON = [ + CONF_BAT_LOW, + CONF_COLD, + CONF_CONNECTED, + CONF_GAS, + CONF_HOT, + CONF_LIGHT, + CONF_LOCKED, + CONF_MOIST, + CONF_MOTION, + CONF_MOVING, + CONF_OCCUPIED, + CONF_OPEN, + CONF_PLUGGED_IN, + CONF_POWERED, + CONF_PRESENT, + CONF_PROBLEM, + CONF_SMOKE, + CONF_SOUND, + CONF_UNSAFE, + CONF_VIBRATION, + CONF_TURNED_ON, +] + +TURNED_OFF = [ + CONF_NOT_BAT_LOW, + CONF_NOT_COLD, + CONF_NOT_CONNECTED, + CONF_NOT_HOT, + CONF_NOT_LOCKED, + CONF_NOT_MOIST, + CONF_NOT_MOVING, + CONF_NOT_OCCUPIED, + CONF_NOT_OPEN, + CONF_NOT_PLUGGED_IN, + CONF_NOT_POWERED, + CONF_NOT_PRESENT, + CONF_NOT_UNSAFE, + CONF_NO_GAS, + CONF_NO_LIGHT, + CONF_NO_MOTION, + CONF_NO_PROBLEM, + CONF_NO_SMOKE, + CONF_NO_SOUND, + CONF_NO_VIBRATION, + CONF_TURNED_OFF, +] + +ENTITY_CONDITIONS = { + DEVICE_CLASS_BATTERY: [ + {CONF_TYPE: CONF_IS_BAT_LOW}, + {CONF_TYPE: CONF_IS_NOT_BAT_LOW}, + ], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_IS_COLD}, {CONF_TYPE: CONF_IS_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_IS_CONNECTED}, + {CONF_TYPE: CONF_IS_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_GARAGE_DOOR: [ + {CONF_TYPE: CONF_IS_OPEN}, + {CONF_TYPE: CONF_IS_NOT_OPEN}, + ], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}, {CONF_TYPE: CONF_IS_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_IS_HOT}, {CONF_TYPE: CONF_IS_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_IS_LIGHT}, {CONF_TYPE: CONF_IS_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_IS_LOCKED}, {CONF_TYPE: CONF_IS_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_IS_MOIST}, {CONF_TYPE: CONF_IS_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_IS_MOTION}, {CONF_TYPE: CONF_IS_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_IS_MOVING}, {CONF_TYPE: CONF_IS_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_IS_OCCUPIED}, + {CONF_TYPE: CONF_IS_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_PLUG: [ + {CONF_TYPE: CONF_IS_PLUGGED_IN}, + {CONF_TYPE: CONF_IS_NOT_PLUGGED_IN}, + ], + DEVICE_CLASS_POWER: [ + {CONF_TYPE: CONF_IS_POWERED}, + {CONF_TYPE: CONF_IS_NOT_POWERED}, + ], + DEVICE_CLASS_PRESENCE: [ + {CONF_TYPE: CONF_IS_PRESENT}, + {CONF_TYPE: CONF_IS_NOT_PRESENT}, + ], + DEVICE_CLASS_PROBLEM: [ + {CONF_TYPE: CONF_IS_PROBLEM}, + {CONF_TYPE: CONF_IS_NO_PROBLEM}, + ], + DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], + DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], + DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], + DEVICE_CLASS_VIBRATION: [ + {CONF_TYPE: CONF_IS_VIBRATION}, + {CONF_TYPE: CONF_IS_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_ON}, {CONF_TYPE: CONF_IS_OFF}], +} + +ENTITY_TRIGGERS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BAT_LOW}, {CONF_TYPE: CONF_NOT_BAT_LOW}], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_COLD}, {CONF_TYPE: CONF_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_CONNECTED}, + {CONF_TYPE: CONF_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_MOIST}, {CONF_TYPE: CONF_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_MOTION}, {CONF_TYPE: CONF_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_MOVING}, {CONF_TYPE: CONF_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_OCCUPIED}, + {CONF_TYPE: CONF_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], + DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], + DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}], + DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], + DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], + DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], + DEVICE_CLASS_VIBRATION: [ + {CONF_TYPE: CONF_VIBRATION}, + {CONF_TYPE: CONF_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], +} + +CONDITION_SCHEMA = vol.Schema( + { + vol.Required(CONF_CONDITION): "device", + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), + } +) + +TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "device", + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), + } +) + + +def async_condition_from_config(config, config_validation): + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + condition_type = config[CONF_TYPE] + if condition_type in IS_ON: + stat = "on" + else: + stat = "off" + state_config = { + condition.CONF_CONDITION: "state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + condition.CONF_STATE: stat, + } + + return condition.state_from_config(state_config, config_validation) + + +async def async_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + trigger_type = config[CONF_TYPE] + if trigger_type in TURNED_ON: + from_state = "off" + to_state = "on" + else: + from_state = "on" + to_state = "off" + state_config = { + state.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + + return await state.async_trigger(hass, state_config, action, automation_info) + + +def _is_domain(entity, domain): + return split_entity_id(entity.entity_id)[0] == domain + + +async def _async_get_automations(hass, device_id, automation_templates, domain): + """List device automations.""" + automations = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entities = async_entries_for_device(entity_registry, device_id) + domain_entities = [x for x in entities if _is_domain(x, domain)] + for entity in domain_entities: + device_class = DEVICE_CLASS_NONE + entity_id = entity.entity_id + entity = hass.states.get(entity_id) + if entity and ATTR_DEVICE_CLASS in entity.attributes: + device_class = entity.attributes[ATTR_DEVICE_CLASS] + automation_template = automation_templates[device_class] + + for automation in automation_template: + automation = dict(automation) + automation.update(device_id=device_id, entity_id=entity_id, domain=domain) + automations.append(automation) + + return automations + + +async def async_get_conditions(hass, device_id): + """List device conditions.""" + automations = await _async_get_automations( + hass, device_id, ENTITY_CONDITIONS, DOMAIN + ) + for automation in automations: + automation.update(condition="device") + return automations + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + automations = await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, DOMAIN) + for automation in automations: + automation.update(platform="device") + return automations diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json new file mode 100644 index 00000000000000..109a2b1fd45f61 --- /dev/null +++ b/homeassistant/components/binary_sensor/strings.json @@ -0,0 +1,93 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} battery is low", + "is_not_bat_low": "{entity_name} battery is normal", + "is_cold": "{entity_name} is cold", + "is_not_cold": "{entity_name} is not cold", + "is_connected": "{entity_name} is connected", + "is_not_connected": "{entity_name} is disconnected", + "is_gas": "{entity_name} is detecting gas", + "is_no_gas": "{entity_name} is not detecting gas", + "is_hot": "{entity_name} is hot", + "is_not_hot": "{entity_name} is not hot", + "is_light": "{entity_name} is detecting light", + "is_no_light": "{entity_name} is not detecting light", + "is_locked": "{entity_name} is locked", + "is_not_locked": "{entity_name} is unlocked", + "is_moist": "{entity_name} is moist", + "is_not_moist": "{entity_name} is dry", + "is_motion": "{entity_name} is detecting motion", + "is_no_motion": "{entity_name} is not detecting motion", + "is_moving": "{entity_name} is moving", + "is_not_moving": "{entity_name} is not moving", + "is_occupied": "{entity_name} is occupied", + "is_not_occupied": "{entity_name} is not occupied", + "is_plugged_in": "{entity_name} is plugged in", + "is_not_plugged_in": "{entity_name} is unplugged", + "is_powered": "{entity_name} is powered", + "is_not_powered": "{entity_name} is not powered", + "is_present": "{entity_name} is present", + "is_not_present": "{entity_name} is not present", + "is_problem": "{entity_name} is detecting problem", + "is_no_problem": "{entity_name} is not detecting problem", + "is_unsafe": "{entity_name} is unsafe", + "is_not_unsafe": "{entity_name} is safe", + "is_smoke": "{entity_name} is detecting smoke", + "is_no_smoke": "{entity_name} is not detecting smoke", + "is_sound": "{entity_name} is detecting sound", + "is_no_sound": "{entity_name} is not detecting sound", + "is_vibration": "{entity_name} is detecting vibration", + "is_no_vibration": "{entity_name} is not detecting vibration", + "is_open": "{entity_name} is open", + "is_not_open": "{entity_name} is closed", + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" + }, + "trigger_type": { + "bat_low": "{entity_name} battery low", + "not_bat_low": "{entity_name} battery normal", + "cold": "{entity_name} became cold", + "not_cold": "{entity_name} became not cold", + "connected": "{entity_name} connected", + "not_connected": "{entity_name} disconnected", + "gas": "{entity_name} started detecting gas", + "no_gas": "{entity_name} stopped detecting gas", + "hot": "{entity_name} became hot", + "not_hot": "{entity_name} became not hot", + "light": "{entity_name} started detecting light", + "no_light": "{entity_name} stopped detecting light", + "locked": "{entity_name} locked", + "not_locked": "{entity_name} unlocked", + "moist§": "{entity_name} became moist", + "not_moist": "{entity_name} became dry", + "motion": "{entity_name} started detecting motion", + "no_motion": "{entity_name} stopped detecting motion", + "moving": "{entity_name} started moving", + "not_moving": "{entity_name} stopped moving", + "occupied": "{entity_name} became occupied", + "not_occupied": "{entity_name} became not occupied", + "plugged_in": "{entity_name} plugged in", + "not_plugged_in": "{entity_name} unplugged", + "powered": "{entity_name} powered", + "not_powered": "{entity_name} not powered", + "present": "{entity_name} present", + "not_present": "{entity_name} not present", + "problem": "{entity_name} started detecting problem", + "no_problem": "{entity_name} stopped detecting problem", + "unsafe": "{entity_name} became unsafe", + "not_unsafe": "{entity_name} became safe", + "smoke": "{entity_name} started detecting smoke", + "no_smoke": "{entity_name} stopped detecting smoke", + "sound": "{entity_name} started detecting sound", + "no_sound": "{entity_name} stopped detecting sound", + "vibration": "{entity_name} started detecting vibration", + "no_vibration": "{entity_name} stopped detecting vibration", + "opened": "{entity_name} opened", + "closed": "{entity_name} closed", + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" + + } + } +} diff --git a/tests/components/binary_sensor/test_device_automation.py b/tests/components/binary_sensor/test_device_automation.py new file mode 100644 index 00000000000000..91124d47f4e4ef --- /dev/null +++ b/tests/components/binary_sensor/test_device_automation.py @@ -0,0 +1,309 @@ +"""The test for binary_sensor device automation.""" +import pytest + +from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.binary_sensor.device_automation import ( + ENTITY_CONDITIONS, + ENTITY_TRIGGERS, +) +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +def _same_lists(a, b): + if len(a) != len(b): + return False + + for d in a: + if d not in b: + return False + return True + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["battery"].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_actions = [] + actions = await async_get_device_automations( + hass, "async_get_actions", device_entry.id + ) + assert _same_lists(actions, expected_actions) + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": condition["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for condition in ENTITY_CONDITIONS[device_class] + ] + conditions = await async_get_device_automations( + hass, "async_get_conditions", device_entry.id + ) + assert _same_lists(conditions, expected_conditions) + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": trigger["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for trigger in ENTITY_TRIGGERS[device_class] + ] + triggers = await async_get_device_automations( + hass, "async_get_triggers", device_entry.id + ) + assert _same_lists(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for on and off triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "bat_low", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "not_bat_low", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "not_bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "not_bat_low state - {} - on - off - None".format( + sensor1.entity_id + ) + + hass.states.async_set(sensor1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "bat_low state - {} - off - on - None".format( + sensor1.entity_id + ) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_bat_low", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_not_bat_low", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/testing_config/custom_components/test/binary_sensor.py b/tests/testing_config/custom_components/test/binary_sensor.py new file mode 100644 index 00000000000000..5052b8e47f10b2 --- /dev/null +++ b/tests/testing_config/custom_components/test/binary_sensor.py @@ -0,0 +1,50 @@ +""" +Provide a mock binary sensor platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.binary_sensor import BinarySensorDevice, DEVICE_CLASSES +from tests.common import MockEntity + + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + device_class: MockBinarySensor( + name=f"{device_class} sensor", + is_on=True, + unique_id=f"unique_{device_class}", + device_class=device_class, + ) + for device_class in DEVICE_CLASSES + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockBinarySensor(MockEntity, BinarySensorDevice): + """Mock Binary Sensor class.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._handle("is_on") + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._handle("device_class") From 2d906b111a932fe74ac30fd79d0633fc7ab8d5eb Mon Sep 17 00:00:00 2001 From: Kevin McCormack Date: Sun, 22 Sep 2019 13:06:02 -0700 Subject: [PATCH 111/296] Update Vivotek camera component (#26754) * Update Vivotek camera component Load model name to instance attribute * Update Vivotek camera component Ensure `update` is called when the entity is added. * Update libpyvivotek to 0.2.2 https://pypi.org/project/libpyvivotek/0.2.2/ - Retrieve camera model name anonymously - Raise a more helpful error for invalid creds --- homeassistant/components/vivotek/camera.py | 9 +++++++-- homeassistant/components/vivotek/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index bf136731cb6a51..012c1e1df34755 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -55,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config[CONF_IP_ADDRESS], ), ) - add_entities([VivotekCam(**args)]) + add_entities([VivotekCam(**args)], True) class VivotekCam(Camera): @@ -68,6 +68,7 @@ def __init__(self, config, cam, stream_source): self._cam = cam self._frame_interval = 1 / config[CONF_FRAMERATE] self._motion_detection_enabled = False + self._model_name = None self._name = config[CONF_NAME] self._stream_source = stream_source @@ -117,4 +118,8 @@ def brand(self): @property def model(self): """Return the camera model.""" - return self._cam.model_name + return self._model_name + + def update(self): + """Update entity status.""" + self._model_name = self._cam.model_name diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index 8a6a37762d4fea..cce2307bc4b554 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -3,7 +3,7 @@ "name": "Vivotek", "documentation": "https://www.home-assistant.io/components/vivotek", "requirements": [ - "libpyvivotek==0.2.1" + "libpyvivotek==0.2.2" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index ad7ba89e4ca618..939fcc27978c75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -732,7 +732,7 @@ libpurecool==0.5.0 libpyfoscam==1.0 # homeassistant.components.vivotek -libpyvivotek==0.2.1 +libpyvivotek==0.2.2 # homeassistant.components.mikrotik librouteros==2.3.0 From 49fef9a6a024790c885704710e18247d0ea63363 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sun, 22 Sep 2019 23:01:32 +0200 Subject: [PATCH 112/296] Add basic support for IKEA Fyrtur blinds (#26659) * Add basic support for IKEA Fyrtur blinds * Update coveragerc * Fix typo * Fix typos * Update following review * Fix incorrect rebase * Fix error * Update to new format of unique id * Add cover * Remove reference to cover in unique id --- .coveragerc | 1 + homeassistant/components/tradfri/__init__.py | 3 + homeassistant/components/tradfri/cover.py | 149 +++++++++++++++++++ homeassistant/components/tradfri/light.py | 11 +- homeassistant/components/tradfri/sensor.py | 8 +- homeassistant/components/tradfri/switch.py | 6 +- 6 files changed, 161 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/tradfri/cover.py diff --git a/.coveragerc b/.coveragerc index a29586c7b6e1cd..302ff9465549e2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -669,6 +669,7 @@ omit = homeassistant/components/trackr/device_tracker.py homeassistant/components/tradfri/* homeassistant/components/tradfri/light.py + homeassistant/components/tradfri/cover.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/transmission/* diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 87b073db052001..bca91134bedf47 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -131,6 +131,9 @@ async def on_hass_stop(event): sw_version=gateway_info.firmware_version, ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "cover") + ) hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "light") ) diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py new file mode 100644 index 00000000000000..3dea978044fcae --- /dev/null +++ b/homeassistant/components/tradfri/cover.py @@ -0,0 +1,149 @@ +"""Support for IKEA Tradfri covers.""" +import logging + +from pytradfri.error import PytradfriError + +from homeassistant.components.cover import ( + CoverDevice, + ATTR_POSITION, + SUPPORT_OPEN, + SUPPORT_CLOSE, + SUPPORT_SET_POSITION, +) +from homeassistant.core import callback +from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY +from .const import CONF_GATEWAY_ID + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Load Tradfri covers based on a config entry.""" + gateway_id = config_entry.data[CONF_GATEWAY_ID] + api = hass.data[KEY_API][config_entry.entry_id] + gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] + + devices_commands = await api(gateway.get_devices()) + devices = await api(devices_commands) + covers = [dev for dev in devices if dev.has_blind_control] + if covers: + async_add_entities(TradfriCover(cover, api, gateway_id) for cover in covers) + + +class TradfriCover(CoverDevice): + """The platform class required by Home Assistant.""" + + def __init__(self, cover, api, gateway_id): + """Initialize a cover.""" + self._api = api + self._unique_id = f"{gateway_id}-{cover.id}" + self._cover = None + self._cover_control = None + self._cover_data = None + self._name = None + self._available = True + self._gateway_id = gateway_id + + self._refresh(cover) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + @property + def unique_id(self): + """Return unique ID for cover.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info.""" + info = self._cover.device_info + + return { + "identifiers": {(TRADFRI_DOMAIN, self._cover.id)}, + "name": self._name, + "manufacturer": info.manufacturer, + "model": info.model_number, + "sw_version": info.firmware_version, + "via_device": (TRADFRI_DOMAIN, self._gateway_id), + } + + async def async_added_to_hass(self): + """Start thread when added to hass.""" + self._async_start_observe() + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def should_poll(self): + """No polling needed for tradfri cover.""" + return False + + @property + def name(self): + """Return the display name of this cover.""" + return self._name + + @property + def current_cover_position(self): + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + return 100 - self._cover_data.current_cover_position + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + await self._api(self._cover_control.set_state(100 - kwargs[ATTR_POSITION])) + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + await self._api(self._cover_control.set_state(0)) + + async def async_close_cover(self, **kwargs): + """Close cover.""" + await self._api(self._cover_control.set_state(100)) + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + return self.current_cover_position == 0 + + @callback + def _async_start_observe(self, exc=None): + """Start observation of cover.""" + if exc: + self._available = False + self.async_schedule_update_ha_state() + _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) + try: + cmd = self._cover.observe( + callback=self._observe_update, + err_callback=self._async_start_observe, + duration=0, + ) + self.hass.async_create_task(self._api(cmd)) + except PytradfriError as err: + _LOGGER.warning("Observation failed, trying again", exc_info=err) + self._async_start_observe() + + def _refresh(self, cover): + """Refresh the cover data.""" + self._cover = cover + + # Caching of BlindControl and cover object + self._available = cover.reachable + self._cover_control = cover.blind_control + self._cover_data = cover.blind_control.blinds[0] + self._name = cover.name + + @callback + def _observe_update(self, tradfri_device): + """Receive new state data for this cover.""" + self._refresh(tradfri_device) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 97fdfd9d36d885..615899a98c8d22 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -1,6 +1,9 @@ """Support for IKEA Tradfri lights.""" import logging +from pytradfri.error import PytradfriError + +import homeassistant.util.color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -14,8 +17,6 @@ Light, ) from homeassistant.core import callback -import homeassistant.util.color as color_util - from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY from .const import CONF_GATEWAY_ID, CONF_IMPORT_GROUPS @@ -26,7 +27,6 @@ ATTR_SAT = "saturation" ATTR_TRANSITION_TIME = "transition_time" PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA -IKEA = "IKEA of Sweden" TRADFRI_LIGHT_MANAGER = "Tradfri Light Manager" SUPPORTED_FEATURES = SUPPORT_TRANSITION SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION @@ -113,9 +113,6 @@ async def async_turn_on(self, **kwargs): @callback def _async_start_observe(self, exc=None): """Start observation of light.""" - # pylint: disable=import-error - from pytradfri.error import PytradfriError - if exc: _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) @@ -339,8 +336,6 @@ async def async_turn_on(self, **kwargs): @callback def _async_start_observe(self, exc=None): """Start observation of light.""" - # pylint: disable=import-error - from pytradfri.error import PytradfriError if exc: self._available = False diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 627a98821549ce..4877dbbb541f1a 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,10 +1,11 @@ """Support for IKEA Tradfri sensors.""" -from datetime import timedelta import logging +from datetime import timedelta + +from pytradfri.error import PytradfriError from homeassistant.core import callback from homeassistant.helpers.entity import Entity - from . import KEY_API, KEY_GATEWAY _LOGGER = logging.getLogger(__name__) @@ -79,9 +80,6 @@ def state(self): @callback def _async_start_observe(self, exc=None): """Start observation of light.""" - # pylint: disable=import-error - from pytradfri.error import PytradfriError - if exc: _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 4be72eb7359e64..545c1ad93cec17 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,15 +1,15 @@ """Support for IKEA Tradfri switches.""" import logging +from pytradfri.error import PytradfriError + from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback - from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY from .const import CONF_GATEWAY_ID _LOGGER = logging.getLogger(__name__) -IKEA = "IKEA of Sweden" TRADFRI_SWITCH_MANAGER = "Tradfri Switch Manager" @@ -98,8 +98,6 @@ async def async_turn_on(self, **kwargs): @callback def _async_start_observe(self, exc=None): """Start observation of switch.""" - from pytradfri.error import PytradfriError - if exc: self._available = False self.async_schedule_update_ha_state() From f82f30dc6247b21a4db304140d4a603d018e09b2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 22 Sep 2019 16:47:41 -0500 Subject: [PATCH 113/296] Unload Plex config entries (#26771) * Unload config entries * Await coroutines * Unnecessary ensure --- homeassistant/components/plex/__init__.py | 23 ++++++++++++++++++- homeassistant/components/plex/const.py | 1 + homeassistant/components/plex/media_player.py | 5 +++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 665091d69b9b1b..dd458dda07880a 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,4 +1,5 @@ """Support to embed Plex.""" +import asyncio import logging import plexapi.exceptions @@ -21,6 +22,7 @@ CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, CONF_SERVER, + CONF_SERVER_IDENTIFIER, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, @@ -28,6 +30,7 @@ PLATFORMS, PLEX_MEDIA_PLAYER_OPTIONS, PLEX_SERVER_CONFIG, + REFRESH_LISTENERS, SERVERS, ) from .server import PlexServer @@ -61,7 +64,7 @@ def setup(hass, config): """Set up the Plex component.""" - hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}}) + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}}) plex_config = config.get(PLEX_DOMAIN, {}) if plex_config: @@ -129,3 +132,21 @@ async def async_setup_entry(hass, entry): ) return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + server_id = entry.data[CONF_SERVER_IDENTIFIER] + + cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id) + await hass.async_add_executor_job(cancel) + + tasks = [ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + await asyncio.gather(*tasks) + + hass.data[PLEX_DOMAIN][SERVERS].pop(server_id) + + return True diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index e77ac303bf1a25..478dd3754e7d8f 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -7,6 +7,7 @@ DEFAULT_VERIFY_SSL = True PLATFORMS = ["media_player", "sensor"] +REFRESH_LISTENERS = "refresh_listeners" SERVERS = "servers" PLEX_CONFIG_FILE = "plex.conf" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index bc19ff41dfedff..4d097253ea1a93 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -39,6 +39,7 @@ DOMAIN as PLEX_DOMAIN, NAME_FORMAT, PLEX_MEDIA_PLAYER_OPTIONS, + REFRESH_LISTENERS, SERVERS, ) @@ -71,7 +72,9 @@ def _setup_platform(hass, config_entry, add_entities_callback): plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] plex_clients = {} plex_sessions = {} - track_time_interval(hass, lambda now: update_devices(), timedelta(seconds=10)) + hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = track_time_interval( + hass, lambda now: update_devices(), timedelta(seconds=10) + ) def update_devices(): """Update the devices objects.""" From fbe85a2eb29054abaeb1679211a3a8899e7bfc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 22 Sep 2019 23:49:09 +0200 Subject: [PATCH 114/296] Add Kaiterra integration (#26661) * add Kaiterra integration * fix: split to multiple platforms * fix lint issues * fix formmating * fix: docstrings * fix: pylint issues * Apply suggestions from code review Co-Authored-By: Martin Hjelmare * Adjust code based on suggestions * Update homeassistant/components/kaiterra/sensor.py Co-Authored-By: Martin Hjelmare --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/kaiterra/__init__.py | 92 ++++++++++++++ .../components/kaiterra/air_quality.py | 115 ++++++++++++++++++ homeassistant/components/kaiterra/api_data.py | 109 +++++++++++++++++ homeassistant/components/kaiterra/const.py | 57 +++++++++ .../components/kaiterra/manifest.json | 8 ++ homeassistant/components/kaiterra/sensor.py | 95 +++++++++++++++ requirements_all.txt | 3 + 9 files changed, 481 insertions(+) create mode 100644 homeassistant/components/kaiterra/__init__.py create mode 100644 homeassistant/components/kaiterra/air_quality.py create mode 100644 homeassistant/components/kaiterra/api_data.py create mode 100644 homeassistant/components/kaiterra/const.py create mode 100644 homeassistant/components/kaiterra/manifest.json create mode 100644 homeassistant/components/kaiterra/sensor.py diff --git a/.coveragerc b/.coveragerc index 302ff9465549e2..65ee6e9e25958a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -318,6 +318,7 @@ omit = homeassistant/components/itunes/media_player.py homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/* + homeassistant/components/kaiterra/* homeassistant/components/kankun/switch.py homeassistant/components/keba/* homeassistant/components/keenetic_ndms2/device_tracker.py diff --git a/CODEOWNERS b/CODEOWNERS index abd3379221a9a8..640a9a7bcc0ae4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -146,6 +146,7 @@ homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi +homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/keba/* @dannerph homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills diff --git a/homeassistant/components/kaiterra/__init__.py b/homeassistant/components/kaiterra/__init__.py new file mode 100644 index 00000000000000..8c61ad5418481a --- /dev/null +++ b/homeassistant/components/kaiterra/__init__.py @@ -0,0 +1,92 @@ +"""Support for Kaiterra devices.""" +import voluptuous as vol + +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers import config_validation as cv + +from homeassistant.const import ( + CONF_API_KEY, + CONF_DEVICES, + CONF_DEVICE_ID, + CONF_SCAN_INTERVAL, + CONF_TYPE, + CONF_NAME, +) + +from .const import ( + AVAILABLE_AQI_STANDARDS, + AVAILABLE_UNITS, + AVAILABLE_DEVICE_TYPES, + CONF_AQI_STANDARD, + CONF_PREFERRED_UNITS, + DOMAIN, + DEFAULT_AQI_STANDARD, + DEFAULT_PREFERRED_UNIT, + DEFAULT_SCAN_INTERVAL, + KAITERRA_COMPONENTS, +) + +from .api_data import KaiterraApiData + +KAITERRA_DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_TYPE): vol.In(AVAILABLE_DEVICE_TYPES), + vol.Optional(CONF_NAME): cv.string, + } +) + +KAITERRA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [KAITERRA_DEVICE_SCHEMA]), + vol.Optional(CONF_AQI_STANDARD, default=DEFAULT_AQI_STANDARD): vol.In( + AVAILABLE_AQI_STANDARDS + ), + vol.Optional(CONF_PREFERRED_UNITS, default=DEFAULT_PREFERRED_UNIT): vol.All( + cv.ensure_list, [vol.In(AVAILABLE_UNITS)] + ), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period, + } +) + +CONFIG_SCHEMA = vol.Schema({DOMAIN: KAITERRA_SCHEMA}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Kaiterra components.""" + + conf = config[DOMAIN] + scan_interval = conf[CONF_SCAN_INTERVAL] + devices = conf[CONF_DEVICES] + session = async_get_clientsession(hass) + api = hass.data[DOMAIN] = KaiterraApiData(hass, conf, session) + + await api.async_update() + + async def _update(now=None): + """Periodic update.""" + await api.async_update() + + async_track_time_interval(hass, _update, scan_interval) + + # Load platforms for each device + for device in devices: + device_name, device_id = ( + device.get(CONF_NAME) or device[CONF_TYPE], + device[CONF_DEVICE_ID], + ) + for component in KAITERRA_COMPONENTS: + hass.async_create_task( + async_load_platform( + hass, + component, + DOMAIN, + {CONF_NAME: device_name, CONF_DEVICE_ID: device_id}, + config, + ) + ) + + return True diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py new file mode 100644 index 00000000000000..4dfe04f9c2e12d --- /dev/null +++ b/homeassistant/components/kaiterra/air_quality.py @@ -0,0 +1,115 @@ +"""Support for Kaiterra Air Quality Sensors.""" +from homeassistant.components.air_quality import AirQualityEntity + +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from homeassistant.const import CONF_DEVICE_ID, CONF_NAME + +from .const import ( + DOMAIN, + ATTR_VOC, + ATTR_AQI_LEVEL, + ATTR_AQI_POLLUTANT, + DISPATCHER_KAITERRA, +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the air_quality kaiterra sensor.""" + if discovery_info is None: + return + + api = hass.data[DOMAIN] + name = discovery_info[CONF_NAME] + device_id = discovery_info[CONF_DEVICE_ID] + + async_add_entities([KaiterraAirQuality(api, name, device_id)]) + + +class KaiterraAirQuality(AirQualityEntity): + """Implementation of a Kaittera air quality sensor.""" + + def __init__(self, api, name, device_id): + """Initialize the sensor.""" + self._api = api + self._name = f"{name} Air Quality" + self._device_id = device_id + + def _data(self, key): + return self._device.get(key, {}).get("value") + + @property + def _device(self): + return self._api.data.get(self._device_id, {}) + + @property + def should_poll(self): + """Return that the sensor should not be polled.""" + return False + + @property + def available(self): + """Return the availability of the sensor.""" + return self._api.data.get(self._device_id) is not None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def air_quality_index(self): + """Return the Air Quality Index (AQI).""" + return self._data("aqi") + + @property + def air_quality_index_level(self): + """Return the Air Quality Index level.""" + return self._data("aqi_level") + + @property + def air_quality_index_pollutant(self): + """Return the Air Quality Index level.""" + return self._data("aqi_pollutant") + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._data("rpm25c") + + @property + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._data("rpm10c") + + @property + def volatile_organic_compounds(self): + """Return the VOC (Volatile Organic Compounds) level.""" + return self._data("rtvoc") + + @property + def unique_id(self): + """Return the sensor's unique id.""" + return f"{self._device_id}_air_quality" + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + data = {} + attributes = [ + (ATTR_VOC, self.volatile_organic_compounds), + (ATTR_AQI_LEVEL, self.air_quality_index_level), + (ATTR_AQI_POLLUTANT, self.air_quality_index_pollutant), + ] + + for attr, value in attributes: + if value is not None: + data[attr] = value + + return data + + async def async_added_to_hass(self): + """Register callback.""" + async_dispatcher_connect( + self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state + ) diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py new file mode 100644 index 00000000000000..0c2d6d9366147d --- /dev/null +++ b/homeassistant/components/kaiterra/api_data.py @@ -0,0 +1,109 @@ +"""Data for all Kaiterra devices.""" +from logging import getLogger + +import asyncio + +import async_timeout + +from aiohttp.client_exceptions import ClientResponseError + +from kaiterra_async_client import KaiterraAPIClient, AQIStandard, Units + +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_DEVICE_ID, CONF_TYPE + +from .const import ( + AQI_SCALE, + AQI_LEVEL, + CONF_AQI_STANDARD, + CONF_PREFERRED_UNITS, + DISPATCHER_KAITERRA, +) + +_LOGGER = getLogger(__name__) + +POLLUTANTS = {"rpm25c": "PM2.5", "rpm10c": "PM10", "rtvoc": "TVOC"} + + +class KaiterraApiData: + """Get data from Kaiterra API.""" + + def __init__(self, hass, config, session): + """Initialize the API data object.""" + + api_key = config[CONF_API_KEY] + aqi_standard = config[CONF_AQI_STANDARD] + devices = config[CONF_DEVICES] + units = config[CONF_PREFERRED_UNITS] + + self._hass = hass + self._api = KaiterraAPIClient( + session, + api_key=api_key, + aqi_standard=AQIStandard.from_str(aqi_standard), + preferred_units=[Units.from_str(unit) for unit in units], + ) + self._devices_ids = [device[CONF_DEVICE_ID] for device in devices] + self._devices = [ + f"/{device[CONF_TYPE]}s/{device[CONF_DEVICE_ID]}" for device in devices + ] + self._scale = AQI_SCALE[aqi_standard] + self._level = AQI_LEVEL[aqi_standard] + self._update_listeners = [] + self.data = {} + + async def async_update(self) -> None: + """Get the data from Kaiterra API.""" + + try: + with async_timeout.timeout(10): + data = await self._api.get_latest_sensor_readings(self._devices) + except (ClientResponseError, asyncio.TimeoutError): + _LOGGER.debug("Couldn't fetch data") + self.data = {} + async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) + + _LOGGER.debug("New data retrieved: %s", data) + + try: + self.data = {} + for i, device in enumerate(data): + if not device: + self.data[self._devices_ids[i]] = {} + continue + + aqi, main_pollutant = None, None + for sensor_name, sensor in device.items(): + points = sensor.get("points") + + if not points: + continue + + point = points[0] + sensor["value"] = point.get("value") + + if "aqi" not in point: + continue + + sensor["aqi"] = point["aqi"] + if not aqi or aqi < point["aqi"]: + aqi = point["aqi"] + main_pollutant = POLLUTANTS.get(sensor_name) + + level = None + for j in range(1, len(self._scale)): + if aqi <= self._scale[j]: + level = self._level[j - 1] + break + + device["aqi"] = {"value": aqi} + device["aqi_level"] = {"value": level} + device["aqi_pollutant"] = {"value": main_pollutant} + + self.data[self._devices_ids[i]] = device + + async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) + except IndexError as err: + _LOGGER.error("Parsing error %s", err) + async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) diff --git a/homeassistant/components/kaiterra/const.py b/homeassistant/components/kaiterra/const.py new file mode 100644 index 00000000000000..7e23edb1259fa9 --- /dev/null +++ b/homeassistant/components/kaiterra/const.py @@ -0,0 +1,57 @@ +"""Consts for Kaiterra integration.""" + +from datetime import timedelta + +DOMAIN = "kaiterra" + +DISPATCHER_KAITERRA = "kaiterra_update" + +AQI_SCALE = { + "cn": [0, 50, 100, 150, 200, 300, 400, 500], + "in": [0, 50, 100, 200, 300, 400, 500], + "us": [0, 50, 100, 150, 200, 300, 500], +} +AQI_LEVEL = { + "cn": [ + "Good", + "Satisfactory", + "Moderate", + "Unhealthy for sensitive groups", + "Unhealthy", + "Very unhealthy", + "Hazardous", + ], + "in": [ + "Good", + "Satisfactory", + "Moderately polluted", + "Poor", + "Very poor", + "Severe", + ], + "us": [ + "Good", + "Moderate", + "Unhealthy for sensitive groups", + "Unhealthy", + "Very unhealthy", + "Hazardous", + ], +} + +ATTR_VOC = "volatile_organic_compounds" +ATTR_AQI_LEVEL = "air_quality_index_level" +ATTR_AQI_POLLUTANT = "air_quality_index_pollutant" + +AVAILABLE_AQI_STANDARDS = ["us", "cn", "in"] +AVAILABLE_UNITS = ["x", "%", "C", "F", "mg/m³", "µg/m³", "ppm", "ppb"] +AVAILABLE_DEVICE_TYPES = ["laseregg", "sensedge"] + +CONF_AQI_STANDARD = "aqi_standard" +CONF_PREFERRED_UNITS = "preferred_units" + +DEFAULT_AQI_STANDARD = "us" +DEFAULT_PREFERRED_UNIT = [] +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +KAITERRA_COMPONENTS = ["sensor", "air_quality"] diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json new file mode 100644 index 00000000000000..926f73fa4dbea9 --- /dev/null +++ b/homeassistant/components/kaiterra/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "kaiterra", + "name": "Kaiterra", + "documentation": "https://www.home-assistant.io/components/kaiterra", + "requirements": ["kaiterra-async-client==0.0.2"], + "codeowners": ["@Michsior14"], + "dependencies": [] +} \ No newline at end of file diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py new file mode 100644 index 00000000000000..4ff6435b64d8b0 --- /dev/null +++ b/homeassistant/components/kaiterra/sensor.py @@ -0,0 +1,95 @@ +"""Support for Kaiterra Temperature ahn Humidity Sensors.""" +from homeassistant.helpers.entity import Entity + +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT + +from .const import DOMAIN, DISPATCHER_KAITERRA + +SENSORS = [ + {"name": "Temperature", "prop": "rtemp", "device_class": "temperature"}, + {"name": "Humidity", "prop": "rhumid", "device_class": "humidity"}, +] + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the kaiterra temperature and humidity sensor.""" + if discovery_info is None: + return + + api = hass.data[DOMAIN] + name = discovery_info[CONF_NAME] + device_id = discovery_info[CONF_DEVICE_ID] + + async_add_entities( + [KaiterraSensor(api, name, device_id, sensor) for sensor in SENSORS] + ) + + +class KaiterraSensor(Entity): + """Implementation of a Kaittera sensor.""" + + def __init__(self, api, name, device_id, sensor): + """Initialize the sensor.""" + self._api = api + self._name = f'{name} {sensor["name"]}' + self._device_id = device_id + self._kind = sensor["name"].lower() + self._property = sensor["prop"] + self._device_class = sensor["device_class"] + + @property + def _sensor(self): + """Return the sensor data.""" + return self._api.data.get(self._device_id, {}).get(self._property, {}) + + @property + def should_poll(self): + """Return that the sensor should not be polled.""" + return False + + @property + def available(self): + """Return the availability of the sensor.""" + return self._api.data.get(self._device_id) is not None + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def state(self): + """Return the state.""" + return self._sensor.get("value") + + @property + def unique_id(self): + """Return the sensor's unique id.""" + return f"{self._device_id}_{self._kind}" + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + if not self._sensor.get("units"): + return None + + value = self._sensor["units"].value + + if value == "F": + return TEMP_FAHRENHEIT + if value == "C": + return TEMP_CELSIUS + return value + + async def async_added_to_hass(self): + """Register callback.""" + async_dispatcher_connect( + self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state + ) diff --git a/requirements_all.txt b/requirements_all.txt index 939fcc27978c75..250643f745d293 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -707,6 +707,9 @@ jsonrpc-async==0.6 # homeassistant.components.kodi jsonrpc-websocket==0.6 +# homeassistant.components.kaiterra +kaiterra-async-client==0.0.2 + # homeassistant.components.keba keba-kecontact==0.2.0 From 5914475fe5f97d08e9d721cbf328727df2f3af5e Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 22 Sep 2019 17:23:14 -0500 Subject: [PATCH 115/296] Add manual step to Plex config flow (#26773) * Add manual config step * Pass token to manual step * Fix for no token * Show error * Specify key location * Cast discovery port to int --- homeassistant/components/plex/config_flow.py | 53 +++++++++++- homeassistant/components/plex/strings.json | 18 ++++- tests/components/plex/test_config_flow.py | 84 ++++++++++++++++++-- 3 files changed, 140 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 3c683c802f5ccf..e620e4869e5083 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -6,13 +6,22 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_URL, CONF_TOKEN, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_URL, + CONF_TOKEN, + CONF_SSL, + CONF_VERIFY_SSL, +) from homeassistant.core import callback from homeassistant.util.json import load_json from .const import ( # pylint: disable=unused-import CONF_SERVER, CONF_SERVER_IDENTIFIER, + DEFAULT_PORT, + DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, PLEX_CONFIG_FILE, @@ -21,7 +30,9 @@ from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer -USER_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): str}) +USER_SCHEMA = vol.Schema( + {vol.Optional(CONF_TOKEN): str, vol.Optional("manual_setup"): bool} +) _LOGGER = logging.getLogger(__package__) @@ -44,14 +55,22 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Plex flow.""" self.current_login = {} + self.discovery_info = {} self.available_servers = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" + errors = {} if user_input is not None: - return await self.async_step_server_validate(user_input) + if user_input.pop("manual_setup", False): + return await self.async_step_manual_setup(user_input) + if CONF_TOKEN in user_input: + return await self.async_step_server_validate(user_input) + errors[CONF_TOKEN] = "no_token" - return self.async_show_form(step_id="user", data_schema=USER_SCHEMA, errors={}) + return self.async_show_form( + step_id="user", data_schema=USER_SCHEMA, errors=errors + ) async def async_step_server_validate(self, server_config): """Validate a provided configuration.""" @@ -114,6 +133,30 @@ async def async_step_server_validate(self, server_config): }, ) + async def async_step_manual_setup(self, user_input=None): + """Begin manual configuration.""" + if len(user_input) > 1: + host = user_input.pop(CONF_HOST) + port = user_input.pop(CONF_PORT) + prefix = "https" if user_input.pop(CONF_SSL) else "http" + user_input[CONF_URL] = f"{prefix}://{host}:{port}" + return await self.async_step_server_validate(user_input) + + data_schema = vol.Schema( + { + vol.Required( + CONF_HOST, default=self.discovery_info.get(CONF_HOST) + ): str, + vol.Required( + CONF_PORT, default=self.discovery_info.get(CONF_PORT, DEFAULT_PORT) + ): int, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, + vol.Optional(CONF_TOKEN, default=user_input.get(CONF_TOKEN, "")): str, + } + ) + return self.async_show_form(step_id="manual_setup", data_schema=data_schema) + async def async_step_select_server(self, user_input=None): """Use selected Plex server.""" config = dict(self.current_login) @@ -148,6 +191,8 @@ async def async_step_discovery(self, discovery_info): # Skip discovery if a config already exists or is in progress. return self.async_abort(reason="already_configured") + discovery_info[CONF_PORT] = int(discovery_info[CONF_PORT]) + self.discovery_info = discovery_info json_file = self.hass.config.path(PLEX_CONFIG_FILE) file_config = await self.hass.async_add_executor_job(load_json, json_file) diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 396a3387fee295..c093d4fe0cec1e 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -2,6 +2,16 @@ "config": { "title": "Plex", "step": { + "manual_setup": { + "title": "Plex server", + "data": { + "host": "Host", + "port": "Port", + "ssl": "Use SSL", + "verify_ssl": "Verify SSL certificate", + "token": "Token (if required)" + } + }, "select_server": { "title": "Select Plex server", "description": "Multiple servers available, select one:", @@ -11,16 +21,18 @@ }, "user": { "title": "Connect Plex server", - "description": "Enter a Plex token for automatic setup.", + "description": "Enter a Plex token for automatic setup or manually configure a server.", "data": { - "token": "Plex token" + "token": "Plex token", + "manual_setup": "Manual setup" } } }, "error": { "faulty_credentials": "Authorization failed", "no_servers": "No servers linked to account", - "not_found": "Plex server not found" + "not_found": "Plex server not found", + "no_token": "Provide a token or select manual setup" }, "abort": { "all_configured": "All linked servers already configured", diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 9c9c1b625259f9..e98aed793cfdca 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -4,7 +4,14 @@ import requests.exceptions from homeassistant.components.plex import config_flow -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, + CONF_TOKEN, + CONF_URL, +) from tests.common import MockConfigEntry @@ -44,7 +51,8 @@ async def test_bad_credentials(hass): ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + result["flow_id"], + user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "form" @@ -196,7 +204,7 @@ async def test_unknown_exception(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"}, - data={CONF_TOKEN: MOCK_TOKEN}, + data={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "abort" @@ -219,7 +227,8 @@ async def test_no_servers_found(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + result["flow_id"], + user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "form" @@ -257,7 +266,8 @@ async def test_single_available_server(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + result["flow_id"], + user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "create_entry" @@ -303,7 +313,8 @@ async def test_multiple_servers_with_selection(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + result["flow_id"], + user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "form" @@ -364,7 +375,8 @@ async def test_adding_last_unconfigured_server(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + result["flow_id"], + user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "create_entry" @@ -447,8 +459,64 @@ async def test_all_available_servers_configured(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + result["flow_id"], + user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "abort" assert result["reason"] == "all_configured" + + +async def test_manual_config(hass): + """Test creating via manual configuration.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: "", "manual_setup": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + + mock_connections = MockConnections(ssl=True) + + with patch("plexapi.server.PlexServer") as mock_plex_server: + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_SERVER_1.clientIdentifier + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_SERVER_1.name + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: MOCK_HOST_1, + CONF_PORT: int(MOCK_PORT_1), + CONF_SSL: True, + CONF_VERIFY_SSL: True, + CONF_TOKEN: MOCK_TOKEN, + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == MOCK_SERVER_1.name + assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == MOCK_SERVER_1.clientIdentifier + ) + assert ( + result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] + == mock_connections.connections[0].httpuri + ) + assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN From 60f0988435ac104f83ace36fa762bb27cb093509 Mon Sep 17 00:00:00 2001 From: Tommy Larsson <45052383+larssont@users.noreply.github.com> Date: Mon, 23 Sep 2019 00:57:39 +0200 Subject: [PATCH 116/296] Add Ombi integration (#26755) * Add Ombi integration * Black * Remove trailing comma * Change dict.get to dict[key] for known keys * Remove monitored conditions from config * Define SCAN_INTERVAL for default scan interval * Adjust requests syntax and add music_requests * Remove Ombi object initialization * Move constants to const.py * Fix imports * Set pyombi requirement to version 0.1.3 * Add services.yaml * Add config schema and setup for integration * Set pyombi requirement to version 0.1.3 * Fix syntax for scan interval * Fix datetime import * Add all files from ombi component * Move imports around * Move imports around * Move pyombi import to top of module * Move scan_interval to sensor module * Add custom validator for urlbase * Add guard clause for discovery_info * Add service validation schemas and constants * Bump pyombi version * Adjust urlbase validation * Add exception warnings for irretrievable media * Bump pyombi * Change from .get to dict[key] * Change schema and conf variable names * Set return to return false * Remove unneeded return --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/ombi/__init__.py | 149 ++++++++++++++++++++ homeassistant/components/ombi/const.py | 24 ++++ homeassistant/components/ombi/manifest.json | 8 ++ homeassistant/components/ombi/sensor.py | 77 ++++++++++ homeassistant/components/ombi/services.yaml | 27 ++++ requirements_all.txt | 3 + 8 files changed, 290 insertions(+) create mode 100644 homeassistant/components/ombi/__init__.py create mode 100644 homeassistant/components/ombi/const.py create mode 100644 homeassistant/components/ombi/manifest.json create mode 100644 homeassistant/components/ombi/sensor.py create mode 100644 homeassistant/components/ombi/services.yaml diff --git a/.coveragerc b/.coveragerc index 65ee6e9e25958a..a4f52af76cefb1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -447,6 +447,7 @@ omit = homeassistant/components/oem/climate.py homeassistant/components/oasa_telematics/sensor.py homeassistant/components/ohmconnect/sensor.py + homeassistant/components/ombi/* homeassistant/components/onewire/sensor.py homeassistant/components/onkyo/media_player.py homeassistant/components/onvif/camera.py diff --git a/CODEOWNERS b/CODEOWNERS index 640a9a7bcc0ae4..8fe47035912d07 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -198,6 +198,7 @@ homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi homeassistant/components/ohmconnect/* @robbiet480 +homeassistant/components/ombi/* @larssont homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py new file mode 100644 index 00000000000000..860c7d4dcb4df0 --- /dev/null +++ b/homeassistant/components/ombi/__init__.py @@ -0,0 +1,149 @@ +"""Support for Ombi.""" +import logging + +import pyombi +import voluptuous as vol + +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) +import homeassistant.helpers.config_validation as cv + +from .const import ( + ATTR_NAME, + ATTR_SEASON, + CONF_URLBASE, + DEFAULT_PORT, + DEFAULT_SEASON, + DEFAULT_SSL, + DEFAULT_URLBASE, + DOMAIN, + SERVICE_MOVIE_REQUEST, + SERVICE_MUSIC_REQUEST, + SERVICE_TV_REQUEST, +) + +_LOGGER = logging.getLogger(__name__) + + +def urlbase(value) -> str: + """Validate and transform urlbase.""" + if value is None: + raise vol.Invalid("string value is None") + value = str(value).strip("/") + if not value: + return value + return value + "/" + + +SUBMIT_MOVIE_REQUEST_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string}) + +SUBMIT_MUSIC_REQUEST_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string}) + +SUBMIT_TV_REQUEST_SERVICE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_NAME): cv.string, + vol.Optional(ATTR_SEASON, default=DEFAULT_SEASON): vol.In( + ["first", "latest", "all"] + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): urlbase, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the Ombi component platform.""" + + ombi = pyombi.Ombi( + ssl=config[DOMAIN][CONF_SSL], + host=config[DOMAIN][CONF_HOST], + port=config[DOMAIN][CONF_PORT], + api_key=config[DOMAIN][CONF_API_KEY], + username=config[DOMAIN][CONF_USERNAME], + urlbase=config[DOMAIN][CONF_URLBASE], + ) + + try: + ombi.test_connection() + except pyombi.OmbiError as err: + _LOGGER.warning("Unable to setup Ombi: %s", err) + return False + + hass.data[DOMAIN] = {"instance": ombi} + + def submit_movie_request(call): + """Submit request for movie.""" + name = call.data[ATTR_NAME] + movies = ombi.search_movie(name) + if movies: + movie = movies[0] + ombi.request_movie(movie["theMovieDbId"]) + else: + raise Warning("No movie found.") + + def submit_tv_request(call): + """Submit request for TV show.""" + name = call.data[ATTR_NAME] + tv_shows = ombi.search_tv(name) + + if tv_shows: + season = call.data[ATTR_SEASON] + show = tv_shows[0]["id"] + if season == "first": + ombi.request_tv(show, request_first=True) + elif season == "latest": + ombi.request_tv(show, request_latest=True) + elif season == "all": + ombi.request_tv(show, request_all=True) + else: + raise Warning("No TV show found.") + + def submit_music_request(call): + """Submit request for music album.""" + name = call.data[ATTR_NAME] + music = ombi.search_music_album(name) + if music: + ombi.request_music(music[0]["foreignAlbumId"]) + else: + raise Warning("No music album found.") + + hass.services.register( + DOMAIN, + SERVICE_MOVIE_REQUEST, + submit_movie_request, + schema=SUBMIT_MOVIE_REQUEST_SERVICE_SCHEMA, + ) + hass.services.register( + DOMAIN, + SERVICE_MUSIC_REQUEST, + submit_music_request, + schema=SUBMIT_MUSIC_REQUEST_SERVICE_SCHEMA, + ) + hass.services.register( + DOMAIN, + SERVICE_TV_REQUEST, + submit_tv_request, + schema=SUBMIT_TV_REQUEST_SERVICE_SCHEMA, + ) + hass.helpers.discovery.load_platform("sensor", DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/ombi/const.py b/homeassistant/components/ombi/const.py new file mode 100644 index 00000000000000..42b58e7f50d631 --- /dev/null +++ b/homeassistant/components/ombi/const.py @@ -0,0 +1,24 @@ +"""Support for Ombi.""" +ATTR_NAME = "name" +ATTR_SEASON = "season" + +CONF_URLBASE = "urlbase" + +DEFAULT_NAME = DOMAIN = "ombi" +DEFAULT_PORT = 5000 +DEFAULT_SEASON = "latest" +DEFAULT_SSL = False +DEFAULT_URLBASE = "" + +SERVICE_MOVIE_REQUEST = "submit_movie_request" +SERVICE_MUSIC_REQUEST = "submit_music_request" +SERVICE_TV_REQUEST = "submit_tv_request" + +SENSOR_TYPES = { + "movies": {"type": "Movie requests", "icon": "mdi:movie"}, + "tv": {"type": "TV show requests", "icon": "mdi:television-classic"}, + "music": {"type": "Music album requests", "icon": "mdi:album"}, + "pending": {"type": "Pending requests", "icon": "mdi:clock-alert-outline"}, + "approved": {"type": "Approved requests", "icon": "mdi:check"}, + "available": {"type": "Available requests", "icon": "mdi:download"}, +} diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json new file mode 100644 index 00000000000000..066f3270ccdd5b --- /dev/null +++ b/homeassistant/components/ombi/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "ombi", + "name": "Ombi", + "documentation": "https://www.home-assistant.io/components/ombi/", + "dependencies": [], + "codeowners": ["@larssont"], + "requirements": ["pyombi==0.1.5"] +} diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py new file mode 100644 index 00000000000000..2a2f50532b4e23 --- /dev/null +++ b/homeassistant/components/ombi/sensor.py @@ -0,0 +1,77 @@ +"""Support for Ombi.""" +from datetime import timedelta +import logging + +from pyombi import OmbiError + +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN, SENSOR_TYPES + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=60) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Ombi sensor platform.""" + if discovery_info is None: + return + + sensors = [] + + ombi = hass.data[DOMAIN]["instance"] + + for sensor in SENSOR_TYPES: + sensor_label = sensor + sensor_type = SENSOR_TYPES[sensor]["type"] + sensor_icon = SENSOR_TYPES[sensor]["icon"] + sensors.append(OmbiSensor(sensor_label, sensor_type, ombi, sensor_icon)) + + add_entities(sensors, True) + + +class OmbiSensor(Entity): + """Representation of an Ombi sensor.""" + + def __init__(self, label, sensor_type, ombi, icon): + """Initialize the sensor.""" + self._state = None + self._label = label + self._type = sensor_type + self._ombi = ombi + self._icon = icon + + @property + def name(self): + """Return the name of the sensor.""" + return f"Ombi {self._type}" + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Update the sensor.""" + try: + if self._label == "movies": + self._state = self._ombi.movie_requests + elif self._label == "tv": + self._state = self._ombi.tv_requests + elif self._label == "music": + self._state = self._ombi.music_requests + elif self._label == "pending": + self._state = self._ombi.total_requests["pending"] + elif self._label == "approved": + self._state = self._ombi.total_requests["approved"] + elif self._label == "available": + self._state = self._ombi.total_requests["available"] + except OmbiError as err: + _LOGGER.warning("Unable to update Ombi sensor: %s", err) + self._state = None diff --git a/homeassistant/components/ombi/services.yaml b/homeassistant/components/ombi/services.yaml new file mode 100644 index 00000000000000..5f4f2defe32097 --- /dev/null +++ b/homeassistant/components/ombi/services.yaml @@ -0,0 +1,27 @@ +# Ombi services.yaml entries + +submit_movie_request: + description: Searches for a movie and requests the first result. + fields: + name: + description: Search parameter + example: "beverly hills cop" + + +submit_tv_request: + description: Searches for a TV show and requests the first result. + fields: + name: + description: Search parameter + example: "breaking bad" + season: + description: Which season(s) to request (first, latest or all) + example: "latest" + + +submit_music_request: + description: Searches for a music album and requests the first result. + fields: + name: + description: Search parameter + example: "nevermind" \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 250643f745d293..614362578e0085 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1353,6 +1353,9 @@ pynzbgetapi==0.2.0 # homeassistant.components.obihai pyobihai==1.1.0 +# homeassistant.components.ombi +pyombi==0.1.5 + # homeassistant.components.openuv pyopenuv==1.0.9 From d162e39ec917922de883813798b99a029c4658a8 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 23 Sep 2019 00:32:13 +0000 Subject: [PATCH 117/296] [ci skip] Translation update --- .../ambiclimate/.translations/no.json | 2 +- .../binary_sensor/.translations/en.json | 92 +++++++++++++++++++ .../binary_sensor/.translations/no.json | 92 +++++++++++++++++++ .../components/deconz/.translations/no.json | 10 +- .../components/deconz/.translations/ru.json | 4 +- .../izone/.translations/zh-Hant.json | 15 +++ .../components/light/.translations/no.json | 4 +- .../components/light/.translations/ru.json | 4 +- .../components/locative/.translations/no.json | 4 +- .../components/plex/.translations/en.json | 14 ++- .../plex/.translations/zh-Hant.json | 33 +++++++ .../components/point/.translations/no.json | 6 +- .../components/switch/.translations/ru.json | 4 +- .../tellduslive/.translations/no.json | 2 +- .../components/toon/.translations/no.json | 4 +- .../components/unifi/.translations/no.json | 6 ++ 16 files changed, 273 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/binary_sensor/.translations/en.json create mode 100644 homeassistant/components/binary_sensor/.translations/no.json create mode 100644 homeassistant/components/izone/.translations/zh-Hant.json create mode 100644 homeassistant/components/plex/.translations/zh-Hant.json diff --git a/homeassistant/components/ambiclimate/.translations/no.json b/homeassistant/components/ambiclimate/.translations/no.json index 567d0b95ff38cb..7bb124ae5433e1 100644 --- a/homeassistant/components/ambiclimate/.translations/no.json +++ b/homeassistant/components/ambiclimate/.translations/no.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og Tillat tilgang til din Ambiclimate konto, og kom s\u00e5 tilbake og trykk Send nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})", + "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og Tillat tilgang til din Ambiclimate konto, kom deretter tilbake og trykk Send nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})", "title": "Autensiere Ambiclimate" } }, diff --git a/homeassistant/components/binary_sensor/.translations/en.json b/homeassistant/components/binary_sensor/.translations/en.json new file mode 100644 index 00000000000000..6379df936b898d --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/en.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} battery is low", + "is_cold": "{entity_name} is cold", + "is_connected": "{entity_name} is connected", + "is_gas": "{entity_name} is detecting gas", + "is_hot": "{entity_name} is hot", + "is_light": "{entity_name} is detecting light", + "is_locked": "{entity_name} is locked", + "is_moist": "{entity_name} is moist", + "is_motion": "{entity_name} is detecting motion", + "is_moving": "{entity_name} is moving", + "is_no_gas": "{entity_name} is not detecting gas", + "is_no_light": "{entity_name} is not detecting light", + "is_no_motion": "{entity_name} is not detecting motion", + "is_no_problem": "{entity_name} is not detecting problem", + "is_no_smoke": "{entity_name} is not detecting smoke", + "is_no_sound": "{entity_name} is not detecting sound", + "is_no_vibration": "{entity_name} is not detecting vibration", + "is_not_bat_low": "{entity_name} battery is normal", + "is_not_cold": "{entity_name} is not cold", + "is_not_connected": "{entity_name} is disconnected", + "is_not_hot": "{entity_name} is not hot", + "is_not_locked": "{entity_name} is unlocked", + "is_not_moist": "{entity_name} is dry", + "is_not_moving": "{entity_name} is not moving", + "is_not_occupied": "{entity_name} is not occupied", + "is_not_open": "{entity_name} is closed", + "is_not_plugged_in": "{entity_name} is unplugged", + "is_not_powered": "{entity_name} is not powered", + "is_not_present": "{entity_name} is not present", + "is_not_unsafe": "{entity_name} is safe", + "is_occupied": "{entity_name} is occupied", + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on", + "is_open": "{entity_name} is open", + "is_plugged_in": "{entity_name} is plugged in", + "is_powered": "{entity_name} is powered", + "is_present": "{entity_name} is present", + "is_problem": "{entity_name} is detecting problem", + "is_smoke": "{entity_name} is detecting smoke", + "is_sound": "{entity_name} is detecting sound", + "is_unsafe": "{entity_name} is unsafe", + "is_vibration": "{entity_name} is detecting vibration" + }, + "trigger_type": { + "bat_low": "{entity_name} battery low", + "closed": "{entity_name} closed", + "cold": "{entity_name} became cold", + "connected": "{entity_name} connected", + "gas": "{entity_name} started detecting gas", + "hot": "{entity_name} became hot", + "light": "{entity_name} started detecting light", + "locked": "{entity_name} locked", + "moist\u00a7": "{entity_name} became moist", + "motion": "{entity_name} started detecting motion", + "moving": "{entity_name} started moving", + "no_gas": "{entity_name} stopped detecting gas", + "no_light": "{entity_name} stopped detecting light", + "no_motion": "{entity_name} stopped detecting motion", + "no_problem": "{entity_name} stopped detecting problem", + "no_smoke": "{entity_name} stopped detecting smoke", + "no_sound": "{entity_name} stopped detecting sound", + "no_vibration": "{entity_name} stopped detecting vibration", + "not_bat_low": "{entity_name} battery normal", + "not_cold": "{entity_name} became not cold", + "not_connected": "{entity_name} disconnected", + "not_hot": "{entity_name} became not hot", + "not_locked": "{entity_name} unlocked", + "not_moist": "{entity_name} became dry", + "not_moving": "{entity_name} stopped moving", + "not_occupied": "{entity_name} became not occupied", + "not_plugged_in": "{entity_name} unplugged", + "not_powered": "{entity_name} not powered", + "not_present": "{entity_name} not present", + "not_unsafe": "{entity_name} became safe", + "occupied": "{entity_name} became occupied", + "opened": "{entity_name} opened", + "plugged_in": "{entity_name} plugged in", + "powered": "{entity_name} powered", + "present": "{entity_name} present", + "problem": "{entity_name} started detecting problem", + "smoke": "{entity_name} started detecting smoke", + "sound": "{entity_name} started detecting sound", + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on", + "unsafe": "{entity_name} became unsafe", + "vibration": "{entity_name} started detecting vibration" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/no.json b/homeassistant/components/binary_sensor/.translations/no.json new file mode 100644 index 00000000000000..5a1916bce59c33 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/no.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batteriniv\u00e5et er lavt", + "is_cold": "{entity_name} er kald", + "is_connected": "{entity_name} er tilkoblet", + "is_gas": "{entity_name} registrerer gass", + "is_hot": "{entity_name} er varm", + "is_light": "{entity_name} registrerer lys", + "is_locked": "{entity_name} er l\u00e5st", + "is_moist": "{entity_name} er fuktig", + "is_motion": "{entity_name} registrerer bevegelse", + "is_moving": "{entity_name} er i bevegelse", + "is_no_gas": "{entity_name} registrerer ikke gass", + "is_no_light": "{entity_name} registrerer ikke lys", + "is_no_motion": "{entity_name} registrerer ikke bevegelse", + "is_no_problem": "{entity_name} registrerer ikke et problem", + "is_no_smoke": "{entity_name} registrerer ikke r\u00f8yk", + "is_no_sound": "{entity_name} registrerer ikke lyd", + "is_no_vibration": "{entity_name} registrerer ikke bevegelse", + "is_not_bat_low": "{entity_name} batteri er normalt", + "is_not_cold": "{entity_name} er ikke kald", + "is_not_connected": "{entity_name} er frakoblet", + "is_not_hot": "{entity_name} er ikke varm", + "is_not_locked": "{entity_name} er ul\u00e5st", + "is_not_moist": "{entity_name} er t\u00f8rr", + "is_not_moving": "{entity_name} er ikke i bevegelse", + "is_not_occupied": "{entity_name} er ledig", + "is_not_open": "{entity_name} er lukket", + "is_not_plugged_in": "{entity_name} er koblet fra", + "is_not_powered": "{entity_name} er spenningsl\u00f8s", + "is_not_present": "{entity_name} er ikke tilstede", + "is_not_unsafe": "{entity_name} er trygg", + "is_occupied": "{entity_name} er opptatt", + "is_off": "{entity_name} er sl\u00e5tt av", + "is_on": "{entity_name} er sl\u00e5tt p\u00e5", + "is_open": "{entity_name} er \u00e5pen", + "is_plugged_in": "{entity_name} er koblet til", + "is_powered": "{entity_name} er spenningssatt", + "is_present": "{entity_name} er tilstede", + "is_problem": "{entity_name} registrerer et problem", + "is_smoke": "{entity_name} registrerer r\u00f8yk", + "is_sound": "{entity_name} registrerer lyd", + "is_unsafe": "{entity_name} er utrygg", + "is_vibration": "{entity_name} registrerer vibrasjon" + }, + "trigger_type": { + "bat_low": "{entity_name} lavt batteri", + "closed": "{entity_name} stengt", + "cold": "{entity_name} ble kald", + "connected": "{entity_name} tilkoblet", + "gas": "{entity_name} begynte \u00e5 registrere gass", + "hot": "{entity_name} ble varm", + "light": "{entity_name} begynte \u00e5 registrere lys", + "locked": "{entity_name} l\u00e5st", + "moist\u00a7": "{entity_name} ble fuktig", + "motion": "{entity_name} begynte \u00e5 registrere bevegelse", + "moving": "{entity_name} begynte \u00e5 bevege seg", + "no_gas": "{entity_name} sluttet \u00e5 registrere gass", + "no_light": "{entity_name} sluttet \u00e5 registrere lys", + "no_motion": "{entity_name} sluttet \u00e5 registrere bevegelse", + "no_problem": "{entity_name} sluttet \u00e5 registrere problem", + "no_smoke": "{entity_name} sluttet \u00e5 registrere r\u00f8yk", + "no_sound": "{entity_name} sluttet \u00e5 registrere lyd", + "no_vibration": "{entity_name} sluttet \u00e5 registrere vibrasjon", + "not_bat_low": "{entity_name} batteri normalt", + "not_cold": "{entity_name} ble ikke lenger kald", + "not_connected": "{entity_name} koblet fra", + "not_hot": "{entity_name} ble ikke lenger varm", + "not_locked": "{entity_name} l\u00e5st opp", + "not_moist": "{entity_name} ble t\u00f8rr", + "not_moving": "{entity_name} sluttet \u00e5 bevege seg", + "not_occupied": "{entity_name} ble ledig", + "not_plugged_in": "{entity_name} koblet fra", + "not_powered": "{entity_name} spenningsl\u00f8s", + "not_present": "{entity_name} ikke til stede", + "not_unsafe": "{entity_name} ble trygg", + "occupied": "{entity_name} ble opptatt", + "opened": "{entity_name} \u00e5pnet", + "plugged_in": "{entity_name} koblet til", + "powered": "{entity_name} spenningssatt", + "present": "{entity_name} tilstede", + "problem": "{entity_name} begynte \u00e5 registrere et problem", + "smoke": "{entity_name} begynte \u00e5 registrere r\u00f8yk", + "sound": "{entity_name} begynte \u00e5 registrere lyd", + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5", + "unsafe": "{entity_name} ble usikker", + "vibration": "{entity_name} begynte \u00e5 oppdage vibrasjon" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 7a93c6ff9cf850..3968c1f00c58d2 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -58,15 +58,15 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { - "remote_button_double_press": "\"{under type}\"-knappen ble dobbeltklikket", - "remote_button_long_press": "\"{undertype}\" - knappen ble kontinuerlig trykket", - "remote_button_long_release": "\" {subtype} \" -knapp sluppet etter langt trykk", + "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", + "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", + "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\"{undertype}\" - knappen femdobbelt klikket", + "remote_button_quintuple_press": "\" {subtype} \" - knappen femdobbelt klikket", "remote_button_rotated": "Knappen roterte \" {subtype} \"", "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", "remote_button_short_release": "\" {subtype} \" -knappen ble utgitt", - "remote_button_triple_press": "\"{under type}\"-knappen trippel klikket", + "remote_button_triple_press": "\" {subtype} \"-knappen trippel klikket", "remote_gyro_activated": "Enhet er ristet" } }, diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 612c5afd033b79..558fd9e5897e7f 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -54,8 +54,8 @@ "left": "\u041d\u0430\u043b\u0435\u0432\u043e", "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", - "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c", - "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c" + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b", diff --git a/homeassistant/components/izone/.translations/zh-Hant.json b/homeassistant/components/izone/.translations/zh-Hant.json new file mode 100644 index 00000000000000..7448100158e8ee --- /dev/null +++ b/homeassistant/components/izone/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 iZone \u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 iZone \u5373\u53ef\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a iZone\uff1f", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/no.json b/homeassistant/components/light/.translations/no.json index 008123739d9138..785e9ca2912e00 100644 --- a/homeassistant/components/light/.translations/no.json +++ b/homeassistant/components/light/.translations/no.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} er p\u00e5" }, "trigger_type": { - "turned_off": "{name} sl\u00e5tt av", - "turned_on": "{name} sl\u00e5tt p\u00e5" + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json index ba9339c1a944e8..a6a7994b7c3603 100644 --- a/homeassistant/components/light/.translations/ru.json +++ b/homeassistant/components/light/.translations/ru.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" } } } \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/no.json b/homeassistant/components/locative/.translations/no.json index 00e3337dfe1eeb..8e9b3272f947b3 100644 --- a/homeassistant/components/locative/.translations/no.json +++ b/homeassistant/components/locative/.translations/no.json @@ -10,9 +10,9 @@ "step": { "user": { "description": "Er du sikker p\u00e5 at du vil sette opp Locative Webhook?", - "title": "Sett opp Lokative Webhook" + "title": "Sett opp Locative Webhook" } }, - "title": "Lokative Webhook" + "title": "Locative Webhook" } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index 2ada5e810ecbaa..7fa9f62be07763 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "Authorization failed", "no_servers": "No servers linked to account", + "no_token": "Provide a token or select manual setup", "not_found": "Plex server not found" }, "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "Use SSL", + "token": "Token (if required)", + "verify_ssl": "Verify SSL certificate" + }, + "title": "Plex server" + }, "select_server": { "data": { "server": "Server" @@ -22,9 +33,10 @@ }, "user": { "data": { + "manual_setup": "Manual setup", "token": "Plex token" }, - "description": "Enter a Plex token for automatic setup.", + "description": "Enter a Plex token for automatic setup or manually configure a server.", "title": "Connect Plex server" } }, diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json new file mode 100644 index 00000000000000..c79a49470e000d --- /dev/null +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "\u6240\u6709\u7d81\u5b9a\u4f3a\u670d\u5668\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", + "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", + "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" + }, + "error": { + "faulty_credentials": "\u9a57\u8b49\u5931\u6557", + "no_servers": "\u6b64\u5e33\u865f\u672a\u7d81\u5b9a\u4f3a\u670d\u5668", + "not_found": "\u627e\u4e0d\u5230 Plex \u4f3a\u670d\u5668" + }, + "step": { + "select_server": { + "data": { + "server": "\u4f3a\u670d\u5668" + }, + "description": "\u627e\u5230\u591a\u500b\u4f3a\u670d\u5668\uff0c\u8acb\u9078\u64c7\u4e00\u7d44\uff1a", + "title": "\u9078\u64c7 Plex \u4f3a\u670d\u5668" + }, + "user": { + "data": { + "token": "Plex \u5bc6\u9470" + }, + "description": "\u8acb\u8f38\u5165 Plex \u5bc6\u9470\u4ee5\u9032\u884c\u81ea\u52d5\u8a2d\u5b9a\u3002", + "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/no.json b/homeassistant/components/point/.translations/no.json index 58b6e1e63fd311..c87c1a702c8d39 100644 --- a/homeassistant/components/point/.translations/no.json +++ b/homeassistant/components/point/.translations/no.json @@ -8,11 +8,11 @@ "no_flows": "Du m\u00e5 konfigurere Point f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/point/)." }, "create_entry": { - "default": "Vellykket godkjenning med Minut for din(e) Point enhet(er)" + "default": "Vellykket autentisering med Minut for din(e) Point enhet(er)" }, "error": { - "follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker p\u00e5 Send", - "no_token": "Ikke godkjent med Minut" + "follow_link": "Vennligst f\u00f8lg lenken og autentiser f\u00f8r du trykker p\u00e5 Send", + "no_token": "Ikke autentisert med Minut" }, "step": { "auth": { diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json index b769e56c97446a..cd5cbc0d6a174b 100644 --- a/homeassistant/components/switch/.translations/ru.json +++ b/homeassistant/components/switch/.translations/ru.json @@ -12,8 +12,8 @@ "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" } } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/no.json b/homeassistant/components/tellduslive/.translations/no.json index d311b3b0d38068..090de51703654f 100644 --- a/homeassistant/components/tellduslive/.translations/no.json +++ b/homeassistant/components/tellduslive/.translations/no.json @@ -12,7 +12,7 @@ "step": { "auth": { "description": "For \u00e5 koble TelldusLive-kontoen din:\n 1. Klikk p\u00e5 linken under\n 2. Logg inn p\u00e5 Telldus Live \n 3. Tillat **{app_name}** (klikk**Ja**). \n 4. Kom tilbake hit og klikk **SUBMIT**. \n\n [Link TelldusLive-konto]({auth_url})", - "title": "Godkjen mot TelldusLive" + "title": "Godkjenn mot TelldusLive" }, "user": { "data": { diff --git a/homeassistant/components/toon/.translations/no.json b/homeassistant/components/toon/.translations/no.json index 37dcd8ac22f9f5..a033d2954d98d1 100644 --- a/homeassistant/components/toon/.translations/no.json +++ b/homeassistant/components/toon/.translations/no.json @@ -8,7 +8,7 @@ "unknown_auth_fail": "Uventet feil oppstod under autentisering." }, "error": { - "credentials": "De oppgitte legitimasjonene er ugyldige.", + "credentials": "Den oppgitte kontoinformasjonen er ugyldig.", "display_exists": "Den valgte skjermen er allerede konfigurert." }, "step": { @@ -18,7 +18,7 @@ "tenant": "Leietaker", "username": "Brukernavn" }, - "description": "Godkjen med Eneco Toon kontoen din (ikke utviklerkontoen).", + "description": "Godkjenn med Eneco Toon kontoen din (ikke utviklerkontoen).", "title": "Linken din Toon konto" }, "display": { diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json index 068f4341544900..c21a47c7ea2c8b 100644 --- a/homeassistant/components/unifi/.translations/no.json +++ b/homeassistant/components/unifi/.translations/no.json @@ -32,6 +32,12 @@ "track_devices": "Spore nettverksenheter (Ubiquiti-enheter)", "track_wired_clients": "Inkluder kablede nettverksklienter" } + }, + "init": { + "data": { + "one": "en", + "other": "andre" + } } } } From 2e4cc7e5a018b5fc3eb81522b7bf893abf94e8e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 Sep 2019 20:46:32 -0700 Subject: [PATCH 118/296] Prevent Wemo doing I/O in event loop (#26835) * Prevent Wemo doing I/O in event loop * Update config_flow.py --- homeassistant/components/wemo/config_flow.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py index a1614eb1ce30c2..21c911a66ce944 100644 --- a/homeassistant/components/wemo/config_flow.py +++ b/homeassistant/components/wemo/config_flow.py @@ -1,14 +1,16 @@ """Config flow for Wemo.""" + +import pywemo + from homeassistant.helpers import config_entry_flow from homeassistant import config_entries + from . import DOMAIN async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - import pywemo - - return bool(pywemo.discover_devices()) + return bool(await hass.async_add_executor_job(pywemo.discover_devices)) config_entry_flow.register_discovery_flow( From 5a4a3e17cc820d28520c47d478b19182527547a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 Sep 2019 20:46:50 -0700 Subject: [PATCH 119/296] Split scaffolding script (#26832) * Add scaffolding split * Add second config flow method --- script/scaffold/__main__.py | 75 +++++-- script/scaffold/docs.py | 22 ++ script/scaffold/error.py | 2 +- script/scaffold/gather_info.py | 188 ++++++++++++++---- script/scaffold/generate.py | 144 ++++++++++---- script/scaffold/model.py | 53 ++++- .../integration/config_flow.py | 13 +- .../tests/test_config_flow.py | 2 +- .../integration/config_flow.py | 18 ++ .../templates/integration/__init__.py | 19 -- .../scaffold/templates/integration/error.py | 10 - .../integration/integration/__init__.py | 12 ++ .../integration/{ => integration}/const.py | 0 .../{ => integration}/manifest.json | 6 +- .../templates/integration/strings.json | 21 -- script/scaffold/templates/tests/__init__.py | 1 - 16 files changed, 426 insertions(+), 160 deletions(-) create mode 100644 script/scaffold/docs.py rename script/scaffold/templates/{ => config_flow}/integration/config_flow.py (83%) rename script/scaffold/templates/{ => config_flow}/tests/test_config_flow.py (97%) create mode 100644 script/scaffold/templates/config_flow_discovery/integration/config_flow.py delete mode 100644 script/scaffold/templates/integration/__init__.py delete mode 100644 script/scaffold/templates/integration/error.py create mode 100644 script/scaffold/templates/integration/integration/__init__.py rename script/scaffold/templates/integration/{ => integration}/const.py (100%) rename script/scaffold/templates/integration/{ => integration}/manifest.json (63%) delete mode 100644 script/scaffold/templates/integration/strings.json delete mode 100644 script/scaffold/templates/tests/__init__.py diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index d1b514ea934a2c..93bcc5aba4172c 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -1,9 +1,42 @@ """Validate manifests.""" +import argparse from pathlib import Path import subprocess import sys -from . import gather_info, generate, error, model +from . import gather_info, generate, error +from .const import COMPONENT_DIR + + +TEMPLATES = [ + p.name for p in (Path(__file__).parent / "templates").glob("*") if p.is_dir() +] + + +def valid_integration(integration): + """Test if it's a valid integration.""" + if not (COMPONENT_DIR / integration).exists(): + raise argparse.ArgumentTypeError( + f"The integration {integration} does not exist." + ) + + return integration + + +def get_arguments() -> argparse.Namespace: + """Get parsed passed in arguments.""" + parser = argparse.ArgumentParser(description="Home Assistant Scaffolder") + parser.add_argument("template", type=str, choices=TEMPLATES) + parser.add_argument( + "--develop", action="store_true", help="Automatically fill in info" + ) + parser.add_argument( + "--integration", type=valid_integration, help="Integration to target." + ) + + arguments = parser.parse_args() + + return arguments def main(): @@ -12,29 +45,22 @@ def main(): print("Run from project root") return 1 - print("Creating a new integration for Home Assistant.") + args = get_arguments() - if "--develop" in sys.argv: - print("Running in developer mode. Automatically filling in info.") - print() + info = gather_info.gather_info(args) - info = model.Info( - domain="develop", - name="Develop Hub", - codeowner="@developer", - requirement="aiodevelop==1.2.3", - ) - else: - try: - info = gather_info.gather_info() - except error.ExitApp as err: - print() - print(err.reason) - return err.exit_code + generate.generate(args.template, info) + + # If creating new integration, create config flow too + if args.template == "integration": + if info.authentication or not info.discoverable: + template = "config_flow" + else: + template = "config_flow_discovery" - generate.generate(info) + generate.generate(template, info) - print("Running hassfest to pick up new codeowner and config flow.") + print("Running hassfest to pick up new information.") subprocess.run("python -m script.hassfest", shell=True) print() @@ -47,10 +73,15 @@ def main(): return 1 print() - print(f"Successfully created the {info.domain} integration!") + print(f"Done!") return 0 if __name__ == "__main__": - sys.exit(main()) + try: + sys.exit(main()) + except error.ExitApp as err: + print() + print(f"Fatal Error: {err.reason}") + sys.exit(err.exit_code) diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py new file mode 100644 index 00000000000000..54a182be31bd63 --- /dev/null +++ b/script/scaffold/docs.py @@ -0,0 +1,22 @@ +"""Print links to relevant docs.""" +from .model import Info + + +def print_relevant_docs(template: str, info: Info) -> None: + """Print relevant docs.""" + if template == "integration": + print( + f""" +Your integration has been created at {info.integration_dir} . Next step is to fill in the blanks for the code marked with TODO. + +For a breakdown of each file, check the developer documentation at: +https://developers.home-assistant.io/docs/en/creating_integration_file_structure.html +""" + ) + + elif template == "config_flow": + print( + f""" +The config flow has been added to the {info.domain} integration. Next step is to fill in the blanks for the code marked with TODO. +""" + ) diff --git a/script/scaffold/error.py b/script/scaffold/error.py index d99cbe8026aff2..75a869572fd7fc 100644 --- a/script/scaffold/error.py +++ b/script/scaffold/error.py @@ -4,7 +4,7 @@ class ExitApp(Exception): """Exception to indicate app should exit.""" - def __init__(self, reason, exit_code): + def __init__(self, reason, exit_code=1): """Initialize the exit app exception.""" self.reason = reason self.exit_code = exit_code diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index 352d1da206c12b..a7263daaf41982 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -1,4 +1,6 @@ """Gather info for scaffolding.""" +import json + from homeassistant.util import slugify from .const import COMPONENT_DIR @@ -9,49 +11,142 @@ CHECK_EMPTY = ["Cannot be empty", lambda value: value] -FIELDS = { - "domain": { - "prompt": "What is the domain?", - "validators": [ - CHECK_EMPTY, - [ - "Domains cannot contain spaces or special characters.", - lambda value: value == slugify(value), - ], - [ - "There already is an integration with this domain.", - lambda value: not (COMPONENT_DIR / value).exists(), - ], - ], - }, - "name": { - "prompt": "What is the name of your integration?", - "validators": [CHECK_EMPTY], - }, - "codeowner": { - "prompt": "What is your GitHub handle?", - "validators": [ - CHECK_EMPTY, - [ - 'GitHub handles need to start with an "@"', - lambda value: value.startswith("@"), - ], - ], - }, - "requirement": { - "prompt": "What PyPI package and version do you depend on? Leave blank for none.", - "validators": [ - ["Versions should be pinned using '=='.", lambda value: "==" in value] - ], - }, -} - - -def gather_info() -> Info: +def gather_info(arguments) -> Info: + """Gather info.""" + existing = arguments.template != "integration" + + if arguments.develop: + print("Running in developer mode. Automatically filling in info.") + print() + + if existing: + if arguments.develop: + return _load_existing_integration("develop") + + if arguments.integration: + return _load_existing_integration(arguments.integration) + + return gather_existing_integration() + + if arguments.develop: + return Info( + domain="develop", + name="Develop Hub", + codeowner="@developer", + requirement="aiodevelop==1.2.3", + ) + + return gather_new_integration() + + +def gather_new_integration() -> Info: + """Gather info about new integration from user.""" + return Info( + **_gather_info( + { + "domain": { + "prompt": "What is the domain?", + "validators": [ + CHECK_EMPTY, + [ + "Domains cannot contain spaces or special characters.", + lambda value: value == slugify(value), + ], + [ + "There already is an integration with this domain.", + lambda value: not (COMPONENT_DIR / value).exists(), + ], + ], + }, + "name": { + "prompt": "What is the name of your integration?", + "validators": [CHECK_EMPTY], + }, + "codeowner": { + "prompt": "What is your GitHub handle?", + "validators": [ + CHECK_EMPTY, + [ + 'GitHub handles need to start with an "@"', + lambda value: value.startswith("@"), + ], + ], + }, + "requirement": { + "prompt": "What PyPI package and version do you depend on? Leave blank for none.", + "validators": [ + [ + "Versions should be pinned using '=='.", + lambda value: not value or "==" in value, + ] + ], + }, + "authentication": { + "prompt": "Does Home Assistant need the user to authenticate to control the device/service? (yes/no)", + "default": "yes", + "validators": [ + [ + "Type either 'yes' or 'no'", + lambda value: value in ("yes", "no"), + ] + ], + "convertor": lambda value: value == "yes", + }, + "discoverable": { + "prompt": "Is the device/service discoverable on the local network? (yes/no)", + "default": "no", + "validators": [ + [ + "Type either 'yes' or 'no'", + lambda value: value in ("yes", "no"), + ] + ], + "convertor": lambda value: value == "yes", + }, + } + ) + ) + + +def gather_existing_integration() -> Info: + """Gather info about existing integration from user.""" + answers = _gather_info( + { + "domain": { + "prompt": "What is the domain?", + "validators": [ + CHECK_EMPTY, + [ + "Domains cannot contain spaces or special characters.", + lambda value: value == slugify(value), + ], + [ + "This integration does not exist.", + lambda value: (COMPONENT_DIR / value).exists(), + ], + ], + } + } + ) + + return _load_existing_integration(answers["domain"]) + + +def _load_existing_integration(domain) -> Info: + """Load an existing integration.""" + if not (COMPONENT_DIR / domain).exists(): + raise ExitApp("Integration does not exist", 1) + + manifest = json.loads((COMPONENT_DIR / domain / "manifest.json").read_text()) + + return Info(domain=domain, name=manifest["name"]) + + +def _gather_info(fields) -> dict: """Gather info from user.""" answers = {} - for key, info in FIELDS.items(): + for key, info in fields.items(): hint = None while key not in answers: if hint is not None: @@ -60,11 +155,18 @@ def gather_info() -> Info: try: print() - value = input(info["prompt"] + "\n> ") + msg = info["prompt"] + if "default" in info: + msg += f" [{info['default']}]" + value = input(f"{msg}\n> ") except (KeyboardInterrupt, EOFError): raise ExitApp("Interrupted!", 1) value = value.strip() + + if value == "" and "default" in info: + value = info["default"] + hint = None for validator_hint, validator in info["validators"]: @@ -73,7 +175,9 @@ def gather_info() -> Info: break if hint is None: + if "convertor" in info: + value = info["convertor"](value) answers[key] = value print() - return Info(**answers) + return answers diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index f7b3f56f2e6623..6bccf6529feeea 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -1,8 +1,7 @@ """Generate an integration.""" -import json from pathlib import Path -from .const import COMPONENT_DIR, TESTS_DIR +from .error import ExitApp from .model import Info TEMPLATE_DIR = Path(__file__).parent / "templates" @@ -10,38 +9,113 @@ TEMPLATE_TESTS = TEMPLATE_DIR / "tests" -def generate(info: Info) -> None: - """Generate an integration.""" - print(f"Generating the {info.domain} integration...") - integration_dir = COMPONENT_DIR / info.domain - test_dir = TESTS_DIR / info.domain - - replaces = { - "NEW_DOMAIN": info.domain, - "NEW_NAME": info.name, - "NEW_CODEOWNER": info.codeowner, - # Special case because we need to keep the list empty if there is none. - '"MANIFEST_NEW_REQUIREMENT"': ( - json.dumps(info.requirement) if info.requirement else "" - ), - } - - for src_dir, target_dir in ( - (TEMPLATE_INTEGRATION, integration_dir), - (TEMPLATE_TESTS, test_dir), - ): - # Guard making it for test purposes. - if not target_dir.exists(): - target_dir.mkdir() - - for source_file in src_dir.glob("**/*"): - content = source_file.read_text() - - for to_search, to_replace in replaces.items(): - content = content.replace(to_search, to_replace) - - target_file = target_dir / source_file.relative_to(src_dir) - print(f"Writing {target_file}") - target_file.write_text(content) +def generate(template: str, info: Info) -> None: + """Generate a template.""" + _validate(template, info) + print(f"Scaffolding {template} for the {info.domain} integration...") + _ensure_tests_dir_exists(info) + _generate(TEMPLATE_DIR / template / "integration", info.integration_dir, info) + _generate(TEMPLATE_DIR / template / "tests", info.tests_dir, info) + _custom_tasks(template, info) print() + + +def _validate(template, info): + """Validate we can run this task.""" + if template == "config_flow": + if (info.integration_dir / "config_flow.py").exists(): + raise ExitApp(f"Integration {info.domain} already has a config flow.") + + +def _generate(src_dir, target_dir, info: Info) -> None: + """Generate an integration.""" + replaces = {"NEW_DOMAIN": info.domain, "NEW_NAME": info.name} + + if not target_dir.exists(): + target_dir.mkdir() + + for source_file in src_dir.glob("**/*"): + content = source_file.read_text() + + for to_search, to_replace in replaces.items(): + content = content.replace(to_search, to_replace) + + target_file = target_dir / source_file.relative_to(src_dir) + print(f"Writing {target_file}") + target_file.write_text(content) + + +def _ensure_tests_dir_exists(info: Info) -> None: + """Ensure a test dir exists.""" + if info.tests_dir.exists(): + return + + info.tests_dir.mkdir() + print(f"Writing {info.tests_dir / '__init__.py'}") + (info.tests_dir / "__init__.py").write_text( + f'"""Tests for the {info.name} integration."""\n' + ) + + +def _custom_tasks(template, info) -> None: + """Handle custom tasks for templates.""" + if template == "integration": + changes = {"codeowners": [info.codeowner]} + + if info.requirement: + changes["requirements"] = [info.requirement] + + info.update_manifest(**changes) + + if template == "config_flow": + info.update_manifest(config_flow=True) + info.update_strings( + config={ + "title": info.name, + "step": { + "user": {"title": "Connect to the device", "data": {"host": "Host"}} + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error", + }, + "abort": {"already_configured": "Device is already configured"}, + } + ) + + if template == "config_flow_discovery": + info.update_manifest(config_flow=True) + info.update_strings( + config={ + "title": info.name, + "step": { + "confirm": { + "title": info.name, + "description": f"Do you want to set up {info.name}?", + } + }, + "abort": { + "single_instance_allowed": f"Only a single configuration of {info.name} is possible.", + "no_devices_found": f"No {info.name} devices found on the network.", + }, + } + ) + + if template in ("config_flow", "config_flow_discovery"): + init_file = info.integration_dir / "__init__.py" + init_file.write_text( + init_file.read_text() + + """ + +async def async_setup_entry(hass, entry): + \"\"\"Set up a config entry for NEW_NAME.\"\"\" + # TODO forward the entry for each platform that you want to set up. + # hass.async_create_task( + # hass.config_entries.async_forward_entry_setup(entry, "media_player") + # ) + + return True +""" + ) diff --git a/script/scaffold/model.py b/script/scaffold/model.py index 83fe922d8c4473..68ab771122e0ab 100644 --- a/script/scaffold/model.py +++ b/script/scaffold/model.py @@ -1,6 +1,11 @@ """Models for scaffolding.""" +import json +from pathlib import Path + import attr +from .const import COMPONENT_DIR, TESTS_DIR + @attr.s class Info: @@ -8,5 +13,49 @@ class Info: domain: str = attr.ib() name: str = attr.ib() - codeowner: str = attr.ib() - requirement: str = attr.ib() + codeowner: str = attr.ib(default=None) + requirement: str = attr.ib(default=None) + authentication: str = attr.ib(default=None) + discoverable: str = attr.ib(default=None) + + @property + def integration_dir(self) -> Path: + """Return directory if integration.""" + return COMPONENT_DIR / self.domain + + @property + def tests_dir(self) -> Path: + """Return test directory.""" + return TESTS_DIR / self.domain + + @property + def manifest_path(self) -> Path: + """Path to the manifest.""" + return COMPONENT_DIR / self.domain / "manifest.json" + + def manifest(self) -> dict: + """Return integration manifest.""" + return json.loads(self.manifest_path.read_text()) + + def update_manifest(self, **kwargs) -> None: + """Update the integration manifest.""" + print(f"Updating {self.domain} manifest: {kwargs}") + self.manifest_path.write_text( + json.dumps({**self.manifest(), **kwargs}, indent=2) + ) + + @property + def strings_path(self) -> Path: + """Path to the strings.""" + return COMPONENT_DIR / self.domain / "strings.json" + + def strings(self) -> dict: + """Return integration strings.""" + if not self.strings_path.exists(): + return {} + return json.loads(self.strings_path.read_text()) + + def update_strings(self, **kwargs) -> None: + """Update the integration strings.""" + print(f"Updating {self.domain} strings: {list(kwargs)}") + self.strings_path.write_text(json.dumps({**self.strings(), **kwargs}, indent=2)) diff --git a/script/scaffold/templates/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py similarity index 83% rename from script/scaffold/templates/integration/config_flow.py rename to script/scaffold/templates/config_flow/integration/config_flow.py index c05141ff0b009a..e08851f47a0358 100644 --- a/script/scaffold/templates/integration/config_flow.py +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -3,10 +3,9 @@ import voluptuous as vol -from homeassistant import core, config_entries +from homeassistant import core, config_entries, exceptions from .const import DOMAIN # pylint:disable=unused-import -from .error import CannotConnect, InvalidAuth _LOGGER = logging.getLogger(__name__) @@ -33,7 +32,7 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for NEW_NAME.""" VERSION = 1 - # TODO pick one of the available connection classes + # TODO pick one of the available connection classes in homeassistant/config_entries.py CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN async def async_step_user(self, user_input=None): @@ -55,3 +54,11 @@ async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/script/scaffold/templates/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py similarity index 97% rename from script/scaffold/templates/tests/test_config_flow.py rename to script/scaffold/templates/config_flow/tests/test_config_flow.py index 7735f497f80176..35d8a96ab2b796 100644 --- a/script/scaffold/templates/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -3,7 +3,7 @@ from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.const import DOMAIN -from homeassistant.components.NEW_DOMAIN.error import CannotConnect, InvalidAuth +from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth from tests.common import mock_coro diff --git a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py new file mode 100644 index 00000000000000..16d13aaa99ffc0 --- /dev/null +++ b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py @@ -0,0 +1,18 @@ +"""Config flow for NEW_NAME.""" +import my_pypi_dependency + +from homeassistant.helpers import config_entry_flow +from homeassistant import config_entries +from .const import DOMAIN + + +async def _async_has_devices(hass) -> bool: + """Return if there are devices that can be discovered.""" + # TODO Check if there are any devices that can be discovered in the network. + devices = await hass.async_add_executor_job(my_pypi_dependency.discover) + return len(devices) > 0 + + +config_entry_flow.register_discovery_flow( + DOMAIN, "NEW_NAME", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN +) diff --git a/script/scaffold/templates/integration/__init__.py b/script/scaffold/templates/integration/__init__.py deleted file mode 100644 index 356c7857d92bdb..00000000000000 --- a/script/scaffold/templates/integration/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -"""The NEW_NAME integration.""" - -from .const import DOMAIN - - -async def async_setup(hass, config): - """Set up the NEW_NAME integration.""" - hass.data[DOMAIN] = config.get(DOMAIN, {}) - return True - - -async def async_setup_entry(hass, entry): - """Set up a config entry for NEW_NAME.""" - # TODO forward the entry for each platform that you want to set up. - # hass.async_create_task( - # hass.config_entries.async_forward_entry_setup(entry, "media_player") - # ) - - return True diff --git a/script/scaffold/templates/integration/error.py b/script/scaffold/templates/integration/error.py deleted file mode 100644 index a99a32bb9501ab..00000000000000 --- a/script/scaffold/templates/integration/error.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Errors for the NEW_NAME integration.""" -from homeassistant.exceptions import HomeAssistantError - - -class CannotConnect(HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class InvalidAuth(HomeAssistantError): - """Error to indicate there is invalid auth.""" diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py new file mode 100644 index 00000000000000..7ab8b736782f62 --- /dev/null +++ b/script/scaffold/templates/integration/integration/__init__.py @@ -0,0 +1,12 @@ +"""The NEW_NAME integration.""" +import voluptuous as vol + +from .const import DOMAIN + + +CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}) + + +async def async_setup(hass, config): + """Set up the NEW_NAME integration.""" + return True diff --git a/script/scaffold/templates/integration/const.py b/script/scaffold/templates/integration/integration/const.py similarity index 100% rename from script/scaffold/templates/integration/const.py rename to script/scaffold/templates/integration/integration/const.py diff --git a/script/scaffold/templates/integration/manifest.json b/script/scaffold/templates/integration/integration/manifest.json similarity index 63% rename from script/scaffold/templates/integration/manifest.json rename to script/scaffold/templates/integration/integration/manifest.json index 7c1e141eef07be..cb4ecac61fb76c 100644 --- a/script/scaffold/templates/integration/manifest.json +++ b/script/scaffold/templates/integration/integration/manifest.json @@ -1,11 +1,11 @@ { "domain": "NEW_DOMAIN", "name": "NEW_NAME", - "config_flow": true, + "config_flow": false, "documentation": "https://www.home-assistant.io/components/NEW_DOMAIN", - "requirements": ["MANIFEST_NEW_REQUIREMENT"], + "requirements": [], "ssdp": {}, "homekit": {}, "dependencies": [], - "codeowners": ["NEW_CODEOWNER"] + "codeowners": [] } diff --git a/script/scaffold/templates/integration/strings.json b/script/scaffold/templates/integration/strings.json deleted file mode 100644 index 0f29967b286547..00000000000000 --- a/script/scaffold/templates/integration/strings.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "title": "NEW_NAME", - "step": { - "user": { - "title": "Connect to the device", - "data": { - "host": "Host" - } - } - }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "Device is already configured" - } - } -} diff --git a/script/scaffold/templates/tests/__init__.py b/script/scaffold/templates/tests/__init__.py deleted file mode 100644 index 081b6d86600012..00000000000000 --- a/script/scaffold/templates/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the NEW_NAME integration.""" From 2f277c4ea7b50ab9624d182bb729cdc2771d0706 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 08:43:55 +0200 Subject: [PATCH 120/296] Remove deprecated ups integration (ADR-0004) (#26824) --- .coveragerc | 1 - homeassistant/components/ups/__init__.py | 1 - homeassistant/components/ups/manifest.json | 10 -- homeassistant/components/ups/sensor.py | 126 --------------------- requirements_all.txt | 3 - 5 files changed, 141 deletions(-) delete mode 100644 homeassistant/components/ups/__init__.py delete mode 100644 homeassistant/components/ups/manifest.json delete mode 100644 homeassistant/components/ups/sensor.py diff --git a/.coveragerc b/.coveragerc index a4f52af76cefb1..38e3917734889d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -690,7 +690,6 @@ omit = homeassistant/components/upcloud/* homeassistant/components/upnp/* homeassistant/components/upc_connect/* - homeassistant/components/ups/sensor.py homeassistant/components/uptimerobot/binary_sensor.py homeassistant/components/uscis/sensor.py homeassistant/components/usps/* diff --git a/homeassistant/components/ups/__init__.py b/homeassistant/components/ups/__init__.py deleted file mode 100644 index 690d3102f9c5bf..00000000000000 --- a/homeassistant/components/ups/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The ups component.""" diff --git a/homeassistant/components/ups/manifest.json b/homeassistant/components/ups/manifest.json deleted file mode 100644 index 98db00c30948e1..00000000000000 --- a/homeassistant/components/ups/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "ups", - "name": "Ups", - "documentation": "https://www.home-assistant.io/components/ups", - "requirements": [ - "upsmychoice==1.0.6" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/ups/sensor.py b/homeassistant/components/ups/sensor.py deleted file mode 100644 index cfe35a9a63fc0c..00000000000000 --- a/homeassistant/components/ups/sensor.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Sensor for UPS packages.""" -import logging -from collections import defaultdict -from datetime import timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_NAME, - CONF_PASSWORD, - CONF_SCAN_INTERVAL, - CONF_USERNAME, -) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle, slugify -from homeassistant.util.dt import now, parse_date - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "ups" -COOKIE = "upsmychoice_cookies.pickle" -ICON = "mdi:package-variant-closed" -STATUS_DELIVERED = "delivered" - -SCAN_INTERVAL = timedelta(seconds=1800) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the UPS platform.""" - import upsmychoice - - _LOGGER.warning( - "The ups integration is deprecated and will be removed " - "in Home Assistant 0.100.0. For more information see ADR-0004:" - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - try: - cookie = hass.config.path(COOKIE) - session = upsmychoice.get_session( - config.get(CONF_USERNAME), config.get(CONF_PASSWORD), cookie_path=cookie - ) - except upsmychoice.UPSError: - _LOGGER.exception("Could not connect to UPS My Choice") - return False - - add_entities( - [ - UPSSensor( - session, - config.get(CONF_NAME), - config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), - ) - ], - True, - ) - - -class UPSSensor(Entity): - """UPS Sensor.""" - - def __init__(self, session, name, interval): - """Initialize the sensor.""" - self._session = session - self._name = name - self._attributes = None - self._state = None - self.update = Throttle(interval)(self._update) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name or DOMAIN - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "packages" - - def _update(self): - """Update device state.""" - import upsmychoice - - status_counts = defaultdict(int) - try: - for package in upsmychoice.get_packages(self._session): - status = slugify(package["status"]) - skip = ( - status == STATUS_DELIVERED - and parse_date(package["delivery_date"]) < now().date() - ) - if skip: - continue - status_counts[status] += 1 - except upsmychoice.UPSError: - _LOGGER.error("Could not connect to UPS My Choice account") - - self._attributes = {ATTR_ATTRIBUTION: upsmychoice.ATTRIBUTION} - self._attributes.update(status_counts) - self._state = sum(status_counts.values()) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @property - def icon(self): - """Icon to use in the frontend.""" - return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 614362578e0085..0ca24c8e9d4df8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1913,9 +1913,6 @@ twilio==6.19.1 # homeassistant.components.upcloud upcloud-api==0.4.3 -# homeassistant.components.ups -upsmychoice==1.0.6 - # homeassistant.components.uscis uscisstatus==0.1.1 From 38ad573b962aeca7ffec9c226c6524111ff88d5b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 08:44:17 +0200 Subject: [PATCH 121/296] Remove deprecated usps integration (ADR-0004) (#26823) --- .coveragerc | 1 - homeassistant/components/usps/__init__.py | 96 --------------- homeassistant/components/usps/camera.py | 88 -------------- homeassistant/components/usps/manifest.json | 10 -- homeassistant/components/usps/sensor.py | 122 -------------------- requirements_all.txt | 3 - 6 files changed, 320 deletions(-) delete mode 100644 homeassistant/components/usps/__init__.py delete mode 100644 homeassistant/components/usps/camera.py delete mode 100644 homeassistant/components/usps/manifest.json delete mode 100644 homeassistant/components/usps/sensor.py diff --git a/.coveragerc b/.coveragerc index 38e3917734889d..06177f069c0799 100644 --- a/.coveragerc +++ b/.coveragerc @@ -692,7 +692,6 @@ omit = homeassistant/components/upc_connect/* homeassistant/components/uptimerobot/binary_sensor.py homeassistant/components/uscis/sensor.py - homeassistant/components/usps/* homeassistant/components/vallox/* homeassistant/components/vasttrafik/sensor.py homeassistant/components/velbus/__init__.py diff --git a/homeassistant/components/usps/__init__.py b/homeassistant/components/usps/__init__.py deleted file mode 100644 index 61da78fa6d7194..00000000000000 --- a/homeassistant/components/usps/__init__.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Support for USPS packages and mail.""" -from datetime import timedelta -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.util import Throttle -from homeassistant.util.dt import now - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "usps" -DATA_USPS = "data_usps" -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) -COOKIE = "usps_cookies.pickle" -CACHE = "usps_cache" -CONF_DRIVER = "driver" - -USPS_TYPE = ["sensor", "camera"] - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DOMAIN): cv.string, - vol.Optional(CONF_DRIVER): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -def setup(hass, config): - """Use config values to set up a function enabling status retrieval.""" - _LOGGER.warning( - "The usps integration is deprecated and will be removed " - "in Home Assistant 0.100.0. For more information see ADR-0004:" - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - name = conf.get(CONF_NAME) - driver = conf.get(CONF_DRIVER) - - import myusps - - try: - cookie = hass.config.path(COOKIE) - cache = hass.config.path(CACHE) - session = myusps.get_session( - username, password, cookie_path=cookie, cache_path=cache, driver=driver - ) - except myusps.USPSError: - _LOGGER.exception("Could not connect to My USPS") - return False - - hass.data[DATA_USPS] = USPSData(session, name) - - for component in USPS_TYPE: - discovery.load_platform(hass, component, DOMAIN, {}, config) - - return True - - -class USPSData: - """Stores the data retrieved from USPS. - - For each entity to use, acts as the single point responsible for fetching - updates from the server. - """ - - def __init__(self, session, name): - """Initialize the data object.""" - self.session = session - self.name = name - self.packages = [] - self.mail = [] - self.attribution = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self, **kwargs): - """Fetch the latest info from USPS.""" - import myusps - - self.packages = myusps.get_packages(self.session) - self.mail = myusps.get_mail(self.session, now().date()) - self.attribution = myusps.ATTRIBUTION - _LOGGER.debug("Mail, request date: %s, list: %s", now().date(), self.mail) - _LOGGER.debug("Package list: %s", self.packages) diff --git a/homeassistant/components/usps/camera.py b/homeassistant/components/usps/camera.py deleted file mode 100644 index 3141314b049cb5..00000000000000 --- a/homeassistant/components/usps/camera.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Support for a camera made up of USPS mail images.""" -from datetime import timedelta -import logging - -from homeassistant.components.camera import Camera - -from . import DATA_USPS - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=10) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up USPS mail camera.""" - if discovery_info is None: - return - - usps = hass.data[DATA_USPS] - add_entities([USPSCamera(usps)]) - - -class USPSCamera(Camera): - """Representation of the images available from USPS.""" - - def __init__(self, usps): - """Initialize the USPS camera images.""" - super().__init__() - - self._usps = usps - self._name = self._usps.name - self._session = self._usps.session - - self._mail_img = [] - self._last_mail = None - self._mail_index = 0 - self._mail_count = 0 - - self._timer = None - - def camera_image(self): - """Update the camera's image if it has changed.""" - self._usps.update() - try: - self._mail_count = len(self._usps.mail) - except TypeError: - # No mail - return None - - if self._usps.mail != self._last_mail: - # Mail items must have changed - self._mail_img = [] - if len(self._usps.mail) >= 1: - self._last_mail = self._usps.mail - for article in self._usps.mail: - _LOGGER.debug("Fetching article image: %s", article) - img = self._session.get(article["image"]).content - self._mail_img.append(img) - - try: - return self._mail_img[self._mail_index] - except IndexError: - return None - - @property - def name(self): - """Return the name of this camera.""" - return f"{self._name} mail" - - @property - def model(self): - """Return date of mail as model.""" - try: - return "Date: {}".format(str(self._usps.mail[0]["date"])) - except IndexError: - return None - - @property - def should_poll(self): - """Update the mail image index periodically.""" - return True - - def update(self): - """Update mail image index.""" - if self._mail_index < (self._mail_count - 1): - self._mail_index += 1 - else: - self._mail_index = 0 diff --git a/homeassistant/components/usps/manifest.json b/homeassistant/components/usps/manifest.json deleted file mode 100644 index 9e2f8886d3acbd..00000000000000 --- a/homeassistant/components/usps/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "usps", - "name": "Usps", - "documentation": "https://www.home-assistant.io/components/usps", - "requirements": [ - "myusps==1.3.2" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/usps/sensor.py b/homeassistant/components/usps/sensor.py deleted file mode 100644 index 7e26e6c9e5c793..00000000000000 --- a/homeassistant/components/usps/sensor.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Sensor for USPS packages.""" -from collections import defaultdict -import logging - -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DATE -from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify -from homeassistant.util.dt import now - -from . import DATA_USPS - -_LOGGER = logging.getLogger(__name__) - -STATUS_DELIVERED = "delivered" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the USPS platform.""" - if discovery_info is None: - return - - usps = hass.data[DATA_USPS] - add_entities([USPSPackageSensor(usps), USPSMailSensor(usps)], True) - - -class USPSPackageSensor(Entity): - """USPS Package Sensor.""" - - def __init__(self, usps): - """Initialize the sensor.""" - self._usps = usps - self._name = self._usps.name - self._attributes = None - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} packages" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Update device state.""" - self._usps.update() - status_counts = defaultdict(int) - for package in self._usps.packages: - status = slugify(package["primary_status"]) - if status == STATUS_DELIVERED and package["delivery_date"] < now().date(): - continue - status_counts[status] += 1 - self._attributes = {ATTR_ATTRIBUTION: self._usps.attribution} - self._attributes.update(status_counts) - self._state = sum(status_counts.values()) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:package-variant-closed" - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "packages" - - -class USPSMailSensor(Entity): - """USPS Mail Sensor.""" - - def __init__(self, usps): - """Initialize the sensor.""" - self._usps = usps - self._name = self._usps.name - self._attributes = None - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} mail" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Update device state.""" - self._usps.update() - if self._usps.mail is not None: - self._state = len(self._usps.mail) - else: - self._state = 0 - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = {} - attr[ATTR_ATTRIBUTION] = self._usps.attribution - try: - attr[ATTR_DATE] = str(self._usps.mail[0]["date"]) - except IndexError: - pass - return attr - - @property - def icon(self): - """Icon to use in the frontend.""" - return "mdi:mailbox" - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "pieces" diff --git a/requirements_all.txt b/requirements_all.txt index 0ca24c8e9d4df8..0a6f1979a91cd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -836,9 +836,6 @@ mychevy==1.2.0 # homeassistant.components.mycroft mycroftapi==2.0 -# homeassistant.components.usps -myusps==1.3.2 - # homeassistant.components.n26 n26==0.2.7 From 5c7f869f9b22e7afdbae70363972e87d427a5b1b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 08:44:38 +0200 Subject: [PATCH 122/296] Remove deprecated sytadin integration (ADR-0004) (#26819) * Remove deprecated sytadin integration (ADR-0004) * Update code owners file * Cleanup coveragerc --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/sytadin/__init__.py | 1 - .../components/sytadin/manifest.json | 12 -- homeassistant/components/sytadin/sensor.py | 151 ------------------ requirements_all.txt | 1 - 6 files changed, 167 deletions(-) delete mode 100644 homeassistant/components/sytadin/__init__.py delete mode 100644 homeassistant/components/sytadin/manifest.json delete mode 100644 homeassistant/components/sytadin/sensor.py diff --git a/.coveragerc b/.coveragerc index 06177f069c0799..b6bdcc2704372f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -629,7 +629,6 @@ omit = homeassistant/components/synologydsm/sensor.py homeassistant/components/syslog/notify.py homeassistant/components/systemmonitor/sensor.py - homeassistant/components/sytadin/sensor.py homeassistant/components/tado/* homeassistant/components/tado/device_tracker.py homeassistant/components/tahoma/* diff --git a/CODEOWNERS b/CODEOWNERS index 8fe47035912d07..ae072cd092ccb4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -269,7 +269,6 @@ homeassistant/components/switchmate/* @danielhiversen homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff -homeassistant/components/sytadin/* @gautric homeassistant/components/tahoma/* @philklei homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike diff --git a/homeassistant/components/sytadin/__init__.py b/homeassistant/components/sytadin/__init__.py deleted file mode 100644 index 5243fe379a774e..00000000000000 --- a/homeassistant/components/sytadin/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The sytadin component.""" diff --git a/homeassistant/components/sytadin/manifest.json b/homeassistant/components/sytadin/manifest.json deleted file mode 100644 index c1453d88d81448..00000000000000 --- a/homeassistant/components/sytadin/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "sytadin", - "name": "Sytadin", - "documentation": "https://www.home-assistant.io/components/sytadin", - "requirements": [ - "beautifulsoup4==4.8.0" - ], - "dependencies": [], - "codeowners": [ - "@gautric" - ] -} diff --git a/homeassistant/components/sytadin/sensor.py b/homeassistant/components/sytadin/sensor.py deleted file mode 100644 index b7c94933a39974..00000000000000 --- a/homeassistant/components/sytadin/sensor.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Support for Sytadin Traffic, French Traffic Supervision.""" -import logging -import re -from datetime import timedelta - -import requests -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - LENGTH_KILOMETERS, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - ATTR_ATTRIBUTION, -) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -URL = "http://www.sytadin.fr/sys/barometres_de_la_circulation.jsp.html" - -ATTRIBUTION = "Data provided by Direction des routes Île-de-France (DiRIF)" - -DEFAULT_NAME = "Sytadin" -REGEX = r"(\d*\.\d+|\d+)" - -OPTION_TRAFFIC_JAM = "traffic_jam" -OPTION_MEAN_VELOCITY = "mean_velocity" -OPTION_CONGESTION = "congestion" - -SENSOR_TYPES = { - OPTION_CONGESTION: ["Congestion", ""], - OPTION_MEAN_VELOCITY: ["Mean Velocity", LENGTH_KILOMETERS + "/h"], - OPTION_TRAFFIC_JAM: ["Traffic Jam", LENGTH_KILOMETERS], -} - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=[OPTION_TRAFFIC_JAM]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up of the Sytadin Traffic sensor platform.""" - _LOGGER.warning( - "The sytadin integration is deprecated and will be removed " - "in Home Assistant 0.100.0. For more information see ADR-0004:" - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - name = config.get(CONF_NAME) - - sytadin = SytadinData(URL) - - dev = [] - for option in config.get(CONF_MONITORED_CONDITIONS): - _LOGGER.debug("Sensor device - %s", option) - dev.append( - SytadinSensor( - sytadin, name, option, SENSOR_TYPES[option][0], SENSOR_TYPES[option][1] - ) - ) - add_entities(dev, True) - - -class SytadinSensor(Entity): - """Representation of a Sytadin Sensor.""" - - def __init__(self, data, name, sensor_type, option, unit): - """Initialize the sensor.""" - self.data = data - self._state = None - self._name = name - self._option = option - self._type = sensor_type - self._unit = unit - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} {self._option}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - def update(self): - """Fetch new state data for the sensor.""" - self.data.update() - - if self.data is None: - return - - if self._type == OPTION_TRAFFIC_JAM: - self._state = self.data.traffic_jam - elif self._type == OPTION_MEAN_VELOCITY: - self._state = self.data.mean_velocity - elif self._type == OPTION_CONGESTION: - self._state = self.data.congestion - - -class SytadinData: - """The class for handling the data retrieval.""" - - def __init__(self, resource): - """Initialize the data object.""" - self._resource = resource - self.data = None - self.traffic_jam = self.mean_velocity = self.congestion = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from the Sytadin.""" - from bs4 import BeautifulSoup - - try: - raw_html = requests.get(self._resource, timeout=10).text - data = BeautifulSoup(raw_html, "html.parser") - - values = data.select(".barometre_valeur") - parse_traffic_jam = re.search(REGEX, values[0].text) - if parse_traffic_jam: - self.traffic_jam = parse_traffic_jam.group() - parse_mean_velocity = re.search(REGEX, values[1].text) - if parse_mean_velocity: - self.mean_velocity = parse_mean_velocity.group() - parse_congestion = re.search(REGEX, values[2].text) - if parse_congestion: - self.congestion = parse_congestion.group() - except requests.exceptions.ConnectionError: - _LOGGER.error("Connection error") - self.data = None diff --git a/requirements_all.txt b/requirements_all.txt index 0a6f1979a91cd0..a3c1548bc1ac04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -262,7 +262,6 @@ batinfo==0.4.2 # homeassistant.components.linksys_ap # homeassistant.components.scrape -# homeassistant.components.sytadin beautifulsoup4==4.8.0 # homeassistant.components.beewi_smartclim From 5c0fa35d4a6a8733d800884a6e8bd4f1273d0381 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Mon, 23 Sep 2019 11:50:18 +0200 Subject: [PATCH 123/296] Add here_travel_time (#24603) * Add here_travel_time * Bump herepy version to 0.6.2 * Update requirements_all.txt * Disable pylint and catch errors * Add herepy to requirements_test_all * Correctly place test req for herepy * use homeassistant.const.LENGTH_METERS * Implemented Requested Changes * Better error message for cryptic error code * add requested changes * add_entities instead of async * Add route attr and distance in km instead of m * fix linting errors * attribute duration in minutes instead of seconds * Correct pattern for longitude * dont split attribute but rather local var * move strings to const and use travelTime * Add tests * Add route for pedestrian and public * fix public transport route generation * remove print statement * Standalone pytest * Use hass fixture and increase test cov _resolve_zone is redundant * Clean up redundant code * Add type annotations * Readd _resolve_zone and add a test for it * Full test cov * use caplog * Add origin/destination attributes According to https://github.com/home-assistant/home-assistant/pull/24956 * Add mode: bicycle * black * Add mode: publicTransportTimeTable * Fix error for publicTransportTimeTable Switch route_mode and travel_mode in api request. * split up config options * More type hints * implement *_entity_id * align attributes with google_travel_time * route in lib apply requested changes * Update requirements_all.txt * remove DATA_KEY * Use ATTR_MODE * add attribution * Only add attribution if not none * Add debug log for raw response * Add _build_hass_attribution * clearer var names in credentials check * async _are_valid_client_credentials --- CODEOWNERS | 1 + .../components/here_travel_time/__init__.py | 1 + .../components/here_travel_time/manifest.json | 12 + .../components/here_travel_time/sensor.py | 431 ++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/here_travel_time/__init__.py | 1 + .../here_travel_time/test_sensor.py | 947 ++++++++++++++++++ .../attribution_response.json | 276 +++++ .../here_travel_time/bike_response.json | 274 +++++ .../car_enabled_response.json | 298 ++++++ .../here_travel_time/car_response.json | 299 ++++++ .../car_shortest_response.json | 231 +++++ .../here_travel_time/pedestrian_response.json | 308 ++++++ .../here_travel_time/public_response.json | 294 ++++++ .../public_time_table_response.json | 308 ++++++ .../routing_error_invalid_credentials.json | 15 + .../routing_error_no_route_found.json | 21 + .../here_travel_time/truck_response.json | 187 ++++ 20 files changed, 3911 insertions(+) create mode 100755 homeassistant/components/here_travel_time/__init__.py create mode 100755 homeassistant/components/here_travel_time/manifest.json create mode 100755 homeassistant/components/here_travel_time/sensor.py create mode 100644 tests/components/here_travel_time/__init__.py create mode 100644 tests/components/here_travel_time/test_sensor.py create mode 100644 tests/fixtures/here_travel_time/attribution_response.json create mode 100644 tests/fixtures/here_travel_time/bike_response.json create mode 100644 tests/fixtures/here_travel_time/car_enabled_response.json create mode 100644 tests/fixtures/here_travel_time/car_response.json create mode 100644 tests/fixtures/here_travel_time/car_shortest_response.json create mode 100644 tests/fixtures/here_travel_time/pedestrian_response.json create mode 100644 tests/fixtures/here_travel_time/public_response.json create mode 100644 tests/fixtures/here_travel_time/public_time_table_response.json create mode 100644 tests/fixtures/here_travel_time/routing_error_invalid_credentials.json create mode 100644 tests/fixtures/here_travel_time/routing_error_no_route_found.json create mode 100644 tests/fixtures/here_travel_time/truck_response.json diff --git a/CODEOWNERS b/CODEOWNERS index ae072cd092ccb4..7e05cdf0b399e7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -115,6 +115,7 @@ homeassistant/components/gtfs/* @robbiet480 homeassistant/components/harmony/* @ehendrix23 homeassistant/components/hassio/* @home-assistant/hass-io homeassistant/components/heos/* @andrewsayre +homeassistant/components/here_travel_time/* @eifinger homeassistant/components/hikvision/* @mezz64 homeassistant/components/hikvisioncam/* @fbradyirl homeassistant/components/history/* @home-assistant/core diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py new file mode 100755 index 00000000000000..9a5c8ec32aca43 --- /dev/null +++ b/homeassistant/components/here_travel_time/__init__.py @@ -0,0 +1 @@ +"""The here_travel_time component.""" diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json new file mode 100755 index 00000000000000..e26e2e1d6ea57c --- /dev/null +++ b/homeassistant/components/here_travel_time/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "here_travel_time", + "name": "HERE travel time", + "documentation": "https://www.home-assistant.io/components/here_travel_time", + "requirements": [ + "herepy==0.6.3.1" + ], + "dependencies": [], + "codeowners": [ + "@eifinger" + ] + } diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py new file mode 100755 index 00000000000000..ba4908fe85c3ac --- /dev/null +++ b/homeassistant/components/here_travel_time/sensor.py @@ -0,0 +1,431 @@ +"""Support for HERE travel time sensors.""" +from datetime import timedelta +import logging +from typing import Callable, Dict, Optional, Union + +import herepy +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_MODE, + CONF_NAME, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import location +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +CONF_DESTINATION_LATITUDE = "destination_latitude" +CONF_DESTINATION_LONGITUDE = "destination_longitude" +CONF_DESTINATION_ENTITY_ID = "destination_entity_id" +CONF_ORIGIN_LATITUDE = "origin_latitude" +CONF_ORIGIN_LONGITUDE = "origin_longitude" +CONF_ORIGIN_ENTITY_ID = "origin_entity_id" +CONF_APP_ID = "app_id" +CONF_APP_CODE = "app_code" +CONF_TRAFFIC_MODE = "traffic_mode" +CONF_ROUTE_MODE = "route_mode" + +DEFAULT_NAME = "HERE Travel Time" + +TRAVEL_MODE_BICYCLE = "bicycle" +TRAVEL_MODE_CAR = "car" +TRAVEL_MODE_PEDESTRIAN = "pedestrian" +TRAVEL_MODE_PUBLIC = "publicTransport" +TRAVEL_MODE_PUBLIC_TIME_TABLE = "publicTransportTimeTable" +TRAVEL_MODE_TRUCK = "truck" +TRAVEL_MODE = [ + TRAVEL_MODE_BICYCLE, + TRAVEL_MODE_CAR, + TRAVEL_MODE_PEDESTRIAN, + TRAVEL_MODE_PUBLIC, + TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODE_TRUCK, +] + +TRAVEL_MODES_PUBLIC = [TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE] +TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK] +TRAVEL_MODES_NON_VEHICLE = [TRAVEL_MODE_BICYCLE, TRAVEL_MODE_PEDESTRIAN] + +TRAFFIC_MODE_ENABLED = "traffic_enabled" +TRAFFIC_MODE_DISABLED = "traffic_disabled" + +ROUTE_MODE_FASTEST = "fastest" +ROUTE_MODE_SHORTEST = "shortest" +ROUTE_MODE = [ROUTE_MODE_FASTEST, ROUTE_MODE_SHORTEST] + +ICON_BICYCLE = "mdi:bike" +ICON_CAR = "mdi:car" +ICON_PEDESTRIAN = "mdi:walk" +ICON_PUBLIC = "mdi:bus" +ICON_TRUCK = "mdi:truck" + +UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] + +ATTR_DURATION = "duration" +ATTR_DISTANCE = "distance" +ATTR_ROUTE = "route" +ATTR_ORIGIN = "origin" +ATTR_DESTINATION = "destination" + +ATTR_MODE = "mode" +ATTR_UNIT_SYSTEM = CONF_UNIT_SYSTEM +ATTR_TRAFFIC_MODE = CONF_TRAFFIC_MODE + +ATTR_DURATION_IN_TRAFFIC = "duration_in_traffic" +ATTR_ORIGIN_NAME = "origin_name" +ATTR_DESTINATION_NAME = "destination_name" + +UNIT_OF_MEASUREMENT = "min" + +SCAN_INTERVAL = timedelta(minutes=5) + +TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] + +NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" + +COORDINATE_SCHEMA = vol.Schema( + { + vol.Inclusive(CONF_DESTINATION_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_DESTINATION_LONGITUDE, "coordinates"): cv.longitude, + } +) + +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_DESTINATION_LATITUDE, CONF_DESTINATION_ENTITY_ID), + cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_APP_ID): cv.string, + vol.Required(CONF_APP_CODE): cv.string, + vol.Inclusive( + CONF_DESTINATION_LATITUDE, "destination_coordinates" + ): cv.latitude, + vol.Inclusive( + CONF_DESTINATION_LONGITUDE, "destination_coordinates" + ): cv.longitude, + vol.Exclusive(CONF_DESTINATION_LATITUDE, "destination"): cv.latitude, + vol.Exclusive(CONF_DESTINATION_ENTITY_ID, "destination"): cv.entity_id, + vol.Inclusive(CONF_ORIGIN_LATITUDE, "origin_coordinates"): cv.latitude, + vol.Inclusive(CONF_ORIGIN_LONGITUDE, "origin_coordinates"): cv.longitude, + vol.Exclusive(CONF_ORIGIN_LATITUDE, "origin"): cv.latitude, + vol.Exclusive(CONF_ORIGIN_ENTITY_ID, "origin"): cv.entity_id, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODE), + vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In( + ROUTE_MODE + ), + vol.Optional(CONF_TRAFFIC_MODE, default=False): cv.boolean, + vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS), + } + ), +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: Dict[str, Union[str, bool]], + async_add_entities: Callable, + discovery_info: None = None, +) -> None: + """Set up the HERE travel time platform.""" + + app_id = config[CONF_APP_ID] + app_code = config[CONF_APP_CODE] + here_client = herepy.RoutingApi(app_id, app_code) + + if not await hass.async_add_executor_job( + _are_valid_client_credentials, here_client + ): + _LOGGER.error( + "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token." + ) + return + + if config.get(CONF_ORIGIN_LATITUDE) is not None: + origin = f"{config[CONF_ORIGIN_LATITUDE]},{config[CONF_ORIGIN_LONGITUDE]}" + else: + origin = config[CONF_ORIGIN_ENTITY_ID] + + if config.get(CONF_DESTINATION_LATITUDE) is not None: + destination = ( + f"{config[CONF_DESTINATION_LATITUDE]},{config[CONF_DESTINATION_LONGITUDE]}" + ) + else: + destination = config[CONF_DESTINATION_ENTITY_ID] + + travel_mode = config[CONF_MODE] + traffic_mode = config[CONF_TRAFFIC_MODE] + route_mode = config[CONF_ROUTE_MODE] + name = config[CONF_NAME] + units = config.get(CONF_UNIT_SYSTEM, hass.config.units.name) + + here_data = HERETravelTimeData( + here_client, travel_mode, traffic_mode, route_mode, units + ) + + sensor = HERETravelTimeSensor(name, origin, destination, here_data) + + async_add_entities([sensor], True) + + +def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool: + """Check if the provided credentials are correct using defaults.""" + known_working_origin = [38.9, -77.04833] + known_working_destination = [39.0, -77.1] + try: + here_client.car_route( + known_working_origin, + known_working_destination, + [ + herepy.RouteMode[ROUTE_MODE_FASTEST], + herepy.RouteMode[TRAVEL_MODE_CAR], + herepy.RouteMode[TRAFFIC_MODE_DISABLED], + ], + ) + except herepy.InvalidCredentialsError: + return False + return True + + +class HERETravelTimeSensor(Entity): + """Representation of a HERE travel time sensor.""" + + def __init__( + self, name: str, origin: str, destination: str, here_data: "HERETravelTimeData" + ) -> None: + """Initialize the sensor.""" + self._name = name + self._here_data = here_data + self._unit_of_measurement = UNIT_OF_MEASUREMENT + self._origin_entity_id = None + self._destination_entity_id = None + self._attrs = { + ATTR_UNIT_SYSTEM: self._here_data.units, + ATTR_MODE: self._here_data.travel_mode, + ATTR_TRAFFIC_MODE: self._here_data.traffic_mode, + } + + # Check if location is a trackable entity + if origin.split(".", 1)[0] in TRACKABLE_DOMAINS: + self._origin_entity_id = origin + else: + self._here_data.origin = origin + + if destination.split(".", 1)[0] in TRACKABLE_DOMAINS: + self._destination_entity_id = destination + else: + self._here_data.destination = destination + + @property + def state(self) -> Optional[str]: + """Return the state of the sensor.""" + if self._here_data.traffic_mode: + if self._here_data.traffic_time is not None: + return str(round(self._here_data.traffic_time / 60)) + if self._here_data.base_time is not None: + return str(round(self._here_data.base_time / 60)) + + return None + + @property + def name(self) -> str: + """Get the name of the sensor.""" + return self._name + + @property + def device_state_attributes( + self + ) -> Optional[Dict[str, Union[None, float, str, bool]]]: + """Return the state attributes.""" + if self._here_data.base_time is None: + return None + + res = self._attrs + if self._here_data.attribution is not None: + res[ATTR_ATTRIBUTION] = self._here_data.attribution + res[ATTR_DURATION] = self._here_data.base_time / 60 + res[ATTR_DISTANCE] = self._here_data.distance + res[ATTR_ROUTE] = self._here_data.route + res[ATTR_DURATION_IN_TRAFFIC] = self._here_data.traffic_time / 60 + res[ATTR_ORIGIN] = self._here_data.origin + res[ATTR_DESTINATION] = self._here_data.destination + res[ATTR_ORIGIN_NAME] = self._here_data.origin_name + res[ATTR_DESTINATION_NAME] = self._here_data.destination_name + return res + + @property + def unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + @property + def icon(self) -> str: + """Icon to use in the frontend depending on travel_mode.""" + if self._here_data.travel_mode == TRAVEL_MODE_BICYCLE: + return ICON_BICYCLE + if self._here_data.travel_mode == TRAVEL_MODE_PEDESTRIAN: + return ICON_PEDESTRIAN + if self._here_data.travel_mode in TRAVEL_MODES_PUBLIC: + return ICON_PUBLIC + if self._here_data.travel_mode == TRAVEL_MODE_TRUCK: + return ICON_TRUCK + return ICON_CAR + + async def async_update(self) -> None: + """Update Sensor Information.""" + # Convert device_trackers to HERE friendly location + if self._origin_entity_id is not None: + self._here_data.origin = await self._get_location_from_entity( + self._origin_entity_id + ) + + if self._destination_entity_id is not None: + self._here_data.destination = await self._get_location_from_entity( + self._destination_entity_id + ) + + await self.hass.async_add_executor_job(self._here_data.update) + + async def _get_location_from_entity(self, entity_id: str) -> Optional[str]: + """Get the location from the entity state or attributes.""" + entity = self.hass.states.get(entity_id) + + if entity is None: + _LOGGER.error("Unable to find entity %s", entity_id) + return None + + # Check if the entity has location attributes + if location.has_location(entity): + return self._get_location_from_attributes(entity) + + # Check if device is in a zone + zone_entity = self.hass.states.get("zone.{}".format(entity.state)) + if location.has_location(zone_entity): + _LOGGER.debug( + "%s is in %s, getting zone location", entity_id, zone_entity.entity_id + ) + return self._get_location_from_attributes(zone_entity) + + # If zone was not found in state then use the state as the location + if entity_id.startswith("sensor."): + return entity.state + + @staticmethod + def _get_location_from_attributes(entity: State) -> str: + """Get the lat/long string from an entities attributes.""" + attr = entity.attributes + return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) + + +class HERETravelTimeData: + """HERETravelTime data object.""" + + def __init__( + self, + here_client: herepy.RoutingApi, + travel_mode: str, + traffic_mode: bool, + route_mode: str, + units: str, + ) -> None: + """Initialize herepy.""" + self.origin = None + self.destination = None + self.travel_mode = travel_mode + self.traffic_mode = traffic_mode + self.route_mode = route_mode + self.attribution = None + self.traffic_time = None + self.distance = None + self.route = None + self.base_time = None + self.origin_name = None + self.destination_name = None + self.units = units + self._client = here_client + + def update(self) -> None: + """Get the latest data from HERE.""" + if self.traffic_mode: + traffic_mode = TRAFFIC_MODE_ENABLED + else: + traffic_mode = TRAFFIC_MODE_DISABLED + + if self.destination is not None and self.origin is not None: + # Convert location to HERE friendly location + destination = self.destination.split(",") + origin = self.origin.split(",") + + _LOGGER.debug( + "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s", + origin, + destination, + herepy.RouteMode[self.route_mode], + herepy.RouteMode[self.travel_mode], + herepy.RouteMode[traffic_mode], + ) + try: + response = self._client.car_route( + origin, + destination, + [ + herepy.RouteMode[self.route_mode], + herepy.RouteMode[self.travel_mode], + herepy.RouteMode[traffic_mode], + ], + ) + except herepy.NoRouteFoundError: + # Better error message for cryptic no route error codes + _LOGGER.error(NO_ROUTE_ERROR_MESSAGE) + return + + _LOGGER.debug("Raw response is: %s", response.response) + + # pylint: disable=no-member + source_attribution = response.response.get("sourceAttribution") + if source_attribution is not None: + self.attribution = self._build_hass_attribution(source_attribution) + # pylint: disable=no-member + route = response.response["route"] + summary = route[0]["summary"] + waypoint = route[0]["waypoint"] + self.base_time = summary["baseTime"] + if self.travel_mode in TRAVEL_MODES_VEHICLE: + self.traffic_time = summary["trafficTime"] + else: + self.traffic_time = self.base_time + distance = summary["distance"] + if self.units == CONF_UNIT_SYSTEM_IMPERIAL: + # Convert to miles. + self.distance = distance / 1609.344 + else: + # Convert to kilometers + self.distance = distance / 1000 + # pylint: disable=no-member + self.route = response.route_short + self.origin_name = waypoint[0]["mappedRoadName"] + self.destination_name = waypoint[1]["mappedRoadName"] + + @staticmethod + def _build_hass_attribution(source_attribution: Dict) -> Optional[str]: + """Build a hass frontend ready string out of the sourceAttribution.""" + suppliers = source_attribution.get("supplier") + if suppliers is not None: + supplier_titles = [] + for supplier in suppliers: + title = supplier.get("title") + if title is not None: + supplier_titles.append(title) + joined_supplier_titles = ",".join(supplier_titles) + attribution = f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind." + return attribution diff --git a/requirements_all.txt b/requirements_all.txt index a3c1548bc1ac04..bf217688d266e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -619,6 +619,9 @@ hdate==0.9.0 # homeassistant.components.heatmiser heatmiserV3==0.9.1 +# homeassistant.components.here_travel_time +herepy==0.6.3.1 + # homeassistant.components.hikvisioncam hikvision==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b4d7fbc089a0d..9e846c9c416ac1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -171,6 +171,9 @@ hbmqtt==0.9.5 # homeassistant.components.jewish_calendar hdate==0.9.0 +# homeassistant.components.here_travel_time +herepy==0.6.3.1 + # homeassistant.components.pi_hole hole==0.5.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 384d50bccef729..d74a57d678d494 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -87,6 +87,7 @@ "haversine", "hbmqtt", "hdate", + "herepy", "hole", "holidays", "home-assistant-frontend", diff --git a/tests/components/here_travel_time/__init__.py b/tests/components/here_travel_time/__init__.py new file mode 100644 index 00000000000000..ac0ec709654e10 --- /dev/null +++ b/tests/components/here_travel_time/__init__.py @@ -0,0 +1 @@ +"""Tests for here_travel_time component.""" diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py new file mode 100644 index 00000000000000..783209690a389c --- /dev/null +++ b/tests/components/here_travel_time/test_sensor.py @@ -0,0 +1,947 @@ +"""The test for the here_travel_time sensor platform.""" +import logging +from unittest.mock import patch +import urllib + +import herepy +import pytest + +from homeassistant.components.here_travel_time.sensor import ( + ATTR_ATTRIBUTION, + ATTR_DESTINATION, + ATTR_DESTINATION_NAME, + ATTR_DISTANCE, + ATTR_DURATION, + ATTR_DURATION_IN_TRAFFIC, + ATTR_ORIGIN, + ATTR_ORIGIN_NAME, + ATTR_ROUTE, + CONF_MODE, + CONF_TRAFFIC_MODE, + CONF_UNIT_SYSTEM, + ICON_BICYCLE, + ICON_CAR, + ICON_PEDESTRIAN, + ICON_PUBLIC, + ICON_TRUCK, + NO_ROUTE_ERROR_MESSAGE, + ROUTE_MODE_FASTEST, + ROUTE_MODE_SHORTEST, + SCAN_INTERVAL, + TRAFFIC_MODE_DISABLED, + TRAFFIC_MODE_ENABLED, + TRAVEL_MODE_BICYCLE, + TRAVEL_MODE_CAR, + TRAVEL_MODE_PEDESTRIAN, + TRAVEL_MODE_PUBLIC, + TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODE_TRUCK, + UNIT_OF_MEASUREMENT, +) +from homeassistant.const import ATTR_ICON +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed, load_fixture + +DOMAIN = "sensor" + +PLATFORM = "here_travel_time" + +APP_ID = "test" +APP_CODE = "test" + +TRUCK_ORIGIN_LATITUDE = "41.9798" +TRUCK_ORIGIN_LONGITUDE = "-87.8801" +TRUCK_DESTINATION_LATITUDE = "41.9043" +TRUCK_DESTINATION_LONGITUDE = "-87.9216" + +BIKE_ORIGIN_LATITUDE = "41.9798" +BIKE_ORIGIN_LONGITUDE = "-87.8801" +BIKE_DESTINATION_LATITUDE = "41.9043" +BIKE_DESTINATION_LONGITUDE = "-87.9216" + +CAR_ORIGIN_LATITUDE = "38.9" +CAR_ORIGIN_LONGITUDE = "-77.04833" +CAR_DESTINATION_LATITUDE = "39.0" +CAR_DESTINATION_LONGITUDE = "-77.1" + + +def _build_mock_url(origin, destination, modes, app_id, app_code, departure): + """Construct a url for HERE.""" + base_url = "https://route.cit.api.here.com/routing/7.2/calculateroute.json?" + parameters = { + "waypoint0": origin, + "waypoint1": destination, + "mode": ";".join(str(herepy.RouteMode[mode]) for mode in modes), + "app_id": app_id, + "app_code": app_code, + "departure": departure, + } + url = base_url + urllib.parse.urlencode(parameters) + return url + + +def _assert_truck_sensor(sensor): + """Assert that states and attributes are correct for truck_response.""" + assert sensor.state == "14" + assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT + + assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert sensor.attributes.get(ATTR_DURATION) == 13.533333333333333 + assert sensor.attributes.get(ATTR_DISTANCE) == 13.049 + assert sensor.attributes.get(ATTR_ROUTE) == ( + "I-190; I-294 S - Tri-State Tollway; I-290 W - Eisenhower Expy W; " + "IL-64 W - E North Ave; I-290 E - Eisenhower Expy E; I-290" + ) + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 13.533333333333333 + assert sensor.attributes.get(ATTR_ORIGIN) == ",".join( + [TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE] + ) + assert sensor.attributes.get(ATTR_DESTINATION) == ",".join( + [TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE] + ) + assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "" + assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Eisenhower Expy E" + assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_TRUCK + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + + assert sensor.attributes.get(ATTR_ICON) == ICON_TRUCK + + +@pytest.fixture +def requests_mock_credentials_check(requests_mock): + """Add the url used in the api validation to all requests mock.""" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url( + ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), + ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), + modes, + APP_ID, + APP_CODE, + "now", + ) + requests_mock.get( + response_url, text=load_fixture("here_travel_time/car_response.json") + ) + return requests_mock + + +@pytest.fixture +def requests_mock_truck_response(requests_mock_credentials_check): + """Return a requests_mock for truck respones.""" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_TRUCK, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url( + ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]), + ",".join([TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]), + modes, + APP_ID, + APP_CODE, + "now", + ) + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/truck_response.json") + ) + + +@pytest.fixture +def requests_mock_car_disabled_response(requests_mock_credentials_check): + """Return a requests_mock for truck respones.""" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url( + ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), + ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), + modes, + APP_ID, + APP_CODE, + "now", + ) + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/car_response.json") + ) + + +async def test_car(hass, requests_mock_car_disabled_response): + """Test that car works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.state == "30" + assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT + + assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert sensor.attributes.get(ATTR_DURATION) == 30.05 + assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 + assert sensor.attributes.get(ATTR_ROUTE) == ( + "US-29 - K St NW; US-29 - Whitehurst Fwy; " + "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" + ) + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 31.016666666666666 + assert sensor.attributes.get(ATTR_ORIGIN) == ",".join( + [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE] + ) + assert sensor.attributes.get(ATTR_DESTINATION) == ",".join( + [CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE] + ) + assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "22nd St NW" + assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Service Rd S" + assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_CAR + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + + assert sensor.attributes.get(ATTR_ICON) == ICON_CAR + + # Test traffic mode disabled + assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( + ATTR_DURATION_IN_TRAFFIC + ) + + +async def test_traffic_mode_enabled(hass, requests_mock_credentials_check): + """Test that traffic mode enabled works.""" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_ENABLED] + response_url = _build_mock_url( + ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), + ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), + modes, + APP_ID, + APP_CODE, + "now", + ) + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/car_enabled_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "traffic_mode": True, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + + # Test traffic mode enabled + assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( + ATTR_DURATION_IN_TRAFFIC + ) + + +async def test_imperial(hass, requests_mock_car_disabled_response): + """Test that imperial units work.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "unit_system": "imperial", + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.attributes.get(ATTR_DISTANCE) == 14.852635608048994 + + +async def test_route_mode_shortest(hass, requests_mock_credentials_check): + """Test that route mode shortest works.""" + origin = "38.902981,-77.048338" + destination = "39.042158,-77.119116" + modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/car_shortest_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "route_mode": ROUTE_MODE_SHORTEST, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.attributes.get(ATTR_DISTANCE) == 18.388 + + +async def test_route_mode_fastest(hass, requests_mock_credentials_check): + """Test that route mode fastest works.""" + origin = "38.902981,-77.048338" + destination = "39.042158,-77.119116" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_ENABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/car_enabled_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "traffic_mode": True, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.attributes.get(ATTR_DISTANCE) == 23.381 + + +async def test_truck(hass, requests_mock_truck_response): + """Test that truck works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": TRUCK_ORIGIN_LATITUDE, + "origin_longitude": TRUCK_ORIGIN_LONGITUDE, + "destination_latitude": TRUCK_DESTINATION_LATITUDE, + "destination_longitude": TRUCK_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + +async def test_public_transport(hass, requests_mock_credentials_check): + """Test that publicTransport works.""" + origin = "41.9798,-87.8801" + destination = "41.9043,-87.9216" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/public_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_PUBLIC, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.state == "89" + assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT + + assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert sensor.attributes.get(ATTR_DURATION) == 89.16666666666667 + assert sensor.attributes.get(ATTR_DISTANCE) == 22.325 + assert sensor.attributes.get(ATTR_ROUTE) == ( + "332 - Palmer/Schiller; 332 - Cargo Rd./Delta Cargo; " "332 - Palmer/Schiller" + ) + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 89.16666666666667 + assert sensor.attributes.get(ATTR_ORIGIN) == origin + assert sensor.attributes.get(ATTR_DESTINATION) == destination + assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd" + assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "" + assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_PUBLIC + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + + assert sensor.attributes.get(ATTR_ICON) == ICON_PUBLIC + + +async def test_public_transport_time_table(hass, requests_mock_credentials_check): + """Test that publicTransportTimeTable works.""" + origin = "41.9798,-87.8801" + destination = "41.9043,-87.9216" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_travel_time/public_time_table_response.json"), + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.state == "80" + assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT + + assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert sensor.attributes.get(ATTR_DURATION) == 79.73333333333333 + assert sensor.attributes.get(ATTR_DISTANCE) == 14.775 + assert sensor.attributes.get(ATTR_ROUTE) == ( + "330 - Archer/Harlem (Terminal); 309 - Elmhurst Metra Station" + ) + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 79.73333333333333 + assert sensor.attributes.get(ATTR_ORIGIN) == origin + assert sensor.attributes.get(ATTR_DESTINATION) == destination + assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd" + assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "" + assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_PUBLIC_TIME_TABLE + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + + assert sensor.attributes.get(ATTR_ICON) == ICON_PUBLIC + + +async def test_pedestrian(hass, requests_mock_credentials_check): + """Test that pedestrian works.""" + origin = "41.9798,-87.8801" + destination = "41.9043,-87.9216" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PEDESTRIAN, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/pedestrian_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_PEDESTRIAN, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.state == "211" + assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT + + assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert sensor.attributes.get(ATTR_DURATION) == 210.51666666666668 + assert sensor.attributes.get(ATTR_DISTANCE) == 12.533 + assert sensor.attributes.get(ATTR_ROUTE) == ( + "Mannheim Rd; W Belmont Ave; Cullerton St; E Fullerton Ave; " + "La Porte Ave; E Palmer Ave; N Railroad Ave; W North Ave; " + "E North Ave; E Third St" + ) + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 210.51666666666668 + assert sensor.attributes.get(ATTR_ORIGIN) == origin + assert sensor.attributes.get(ATTR_DESTINATION) == destination + assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd" + assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "" + assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_PEDESTRIAN + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + + assert sensor.attributes.get(ATTR_ICON) == ICON_PEDESTRIAN + + +async def test_bicycle(hass, requests_mock_credentials_check): + """Test that bicycle works.""" + origin = "41.9798,-87.8801" + destination = "41.9043,-87.9216" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_BICYCLE, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/bike_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_BICYCLE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.state == "55" + assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT + + assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert sensor.attributes.get(ATTR_DURATION) == 54.86666666666667 + assert sensor.attributes.get(ATTR_DISTANCE) == 12.613 + assert sensor.attributes.get(ATTR_ROUTE) == ( + "Mannheim Rd; W Belmont Ave; Cullerton St; N Landen Dr; " + "E Fullerton Ave; N Wolf Rd; W North Ave; N Clinton Ave; " + "E Third St; N Caroline Ave" + ) + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 54.86666666666667 + assert sensor.attributes.get(ATTR_ORIGIN) == origin + assert sensor.attributes.get(ATTR_DESTINATION) == destination + assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd" + assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "" + assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_BICYCLE + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + + assert sensor.attributes.get(ATTR_ICON) == ICON_BICYCLE + + +async def test_location_zone(hass, requests_mock_truck_response): + """Test that origin/destination supplied by a zone works.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + zone_config = { + "zone": [ + { + "name": "Destination", + "latitude": TRUCK_DESTINATION_LATITUDE, + "longitude": TRUCK_DESTINATION_LONGITUDE, + "radius": 250, + "passive": False, + }, + { + "name": "Origin", + "latitude": TRUCK_ORIGIN_LATITUDE, + "longitude": TRUCK_ORIGIN_LONGITUDE, + "radius": 250, + "passive": False, + }, + ] + } + assert await async_setup_component(hass, "zone", zone_config) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "zone.origin", + "destination_entity_id": "zone.destination", + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + # Test that update works more than once + async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + +async def test_location_sensor(hass, requests_mock_truck_response): + """Test that origin/destination supplied by a sensor works.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + hass.states.async_set( + "sensor.origin", ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]) + ) + hass.states.async_set( + "sensor.destination", + ",".join([TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]), + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "sensor.origin", + "destination_entity_id": "sensor.destination", + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + # Test that update works more than once + async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + +async def test_location_person(hass, requests_mock_truck_response): + """Test that origin/destination supplied by a person works.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + hass.states.async_set( + "person.origin", + "unknown", + { + "latitude": float(TRUCK_ORIGIN_LATITUDE), + "longitude": float(TRUCK_ORIGIN_LONGITUDE), + }, + ) + hass.states.async_set( + "person.destination", + "unknown", + { + "latitude": float(TRUCK_DESTINATION_LATITUDE), + "longitude": float(TRUCK_DESTINATION_LONGITUDE), + }, + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "person.origin", + "destination_entity_id": "person.destination", + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + # Test that update works more than once + async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + +async def test_location_device_tracker(hass, requests_mock_truck_response): + """Test that origin/destination supplied by a device_tracker works.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + hass.states.async_set( + "device_tracker.origin", + "unknown", + { + "latitude": float(TRUCK_ORIGIN_LATITUDE), + "longitude": float(TRUCK_ORIGIN_LONGITUDE), + }, + ) + hass.states.async_set( + "device_tracker.destination", + "unknown", + { + "latitude": float(TRUCK_DESTINATION_LATITUDE), + "longitude": float(TRUCK_DESTINATION_LONGITUDE), + }, + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "device_tracker.origin", + "destination_entity_id": "device_tracker.destination", + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + # Test that update works more than once + async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + +async def test_location_device_tracker_added_after_update( + hass, requests_mock_truck_response, caplog +): + """Test that device_tracker added after first update works.""" + caplog.set_level(logging.ERROR) + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "device_tracker.origin", + "destination_entity_id": "device_tracker.destination", + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert len(caplog.records) == 2 + assert "Unable to find entity" in caplog.text + caplog.clear() + + # Device tracker appear after first update + hass.states.async_set( + "device_tracker.origin", + "unknown", + { + "latitude": float(TRUCK_ORIGIN_LATITUDE), + "longitude": float(TRUCK_ORIGIN_LONGITUDE), + }, + ) + hass.states.async_set( + "device_tracker.destination", + "unknown", + { + "latitude": float(TRUCK_DESTINATION_LATITUDE), + "longitude": float(TRUCK_DESTINATION_LONGITUDE), + }, + ) + + # Test that update works more than once + async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + assert len(caplog.records) == 0 + + +async def test_location_device_tracker_in_zone( + hass, requests_mock_truck_response, caplog +): + """Test that device_tracker in zone uses device_tracker state works.""" + caplog.set_level(logging.DEBUG) + zone_config = { + "zone": [ + { + "name": "Origin", + "latitude": TRUCK_ORIGIN_LATITUDE, + "longitude": TRUCK_ORIGIN_LONGITUDE, + "radius": 250, + "passive": False, + } + ] + } + assert await async_setup_component(hass, "zone", zone_config) + hass.states.async_set( + "device_tracker.origin", "origin", {"latitude": None, "longitude": None} + ) + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "device_tracker.origin", + "destination_latitude": TRUCK_DESTINATION_LATITUDE, + "destination_longitude": TRUCK_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + assert ", getting zone location" in caplog.text + + +async def test_route_not_found(hass, requests_mock_credentials_check, caplog): + """Test that route not found error is correctly handled.""" + caplog.set_level(logging.ERROR) + origin = "52.516,13.3779" + destination = "47.013399,-10.171986" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_travel_time/routing_error_no_route_found.json"), + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert NO_ROUTE_ERROR_MESSAGE in caplog.text + + +async def test_pattern_origin(hass, caplog): + """Test that pattern matching the origin works.""" + caplog.set_level(logging.ERROR) + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": "138.90", + "origin_longitude": "-77.04833", + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert "invalid latitude" in caplog.text + + +async def test_pattern_destination(hass, caplog): + """Test that pattern matching the destination works.""" + caplog.set_level(logging.ERROR) + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": "139.0", + "destination_longitude": "-77.1", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert "invalid latitude" in caplog.text + + +async def test_invalid_credentials(hass, requests_mock, caplog): + """Test that invalid credentials error is correctly handled.""" + caplog.set_level(logging.ERROR) + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url( + ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), + ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), + modes, + APP_ID, + APP_CODE, + "now", + ) + requests_mock.get( + response_url, + text=load_fixture("here_travel_time/routing_error_invalid_credentials.json"), + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert "Invalid credentials" in caplog.text + + +async def test_attribution(hass, requests_mock_credentials_check): + """Test that attributions are correctly displayed.""" + origin = "50.037751372637686,14.39233448220898" + destination = "50.07993838201255,14.42582157361062" + modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_ENABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/attribution_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "traffic_mode": True, + "route_mode": ROUTE_MODE_SHORTEST, + "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + sensor = hass.states.get("sensor.test") + assert ( + sensor.attributes.get(ATTR_ATTRIBUTION) + == "With the support of HERE Technologies. All information is provided without warranty of any kind." + ) diff --git a/tests/fixtures/here_travel_time/attribution_response.json b/tests/fixtures/here_travel_time/attribution_response.json new file mode 100644 index 00000000000000..9b682f6c51fb14 --- /dev/null +++ b/tests/fixtures/here_travel_time/attribution_response.json @@ -0,0 +1,276 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-09-21T15:17:31Z", + "mapVersion": "8.30.100.154", + "moduleVersion": "7.2.201937-5251", + "interfaceVersion": "2.6.70", + "availableMapVersion": [ + "8.30.100.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "+565790671", + "mappedPosition": { + "latitude": 50.0378591, + "longitude": 14.3924721 + }, + "originalPosition": { + "latitude": 50.0377513, + "longitude": 14.3923344 + }, + "type": "stopOver", + "spot": 0.3, + "sideOfStreet": "left", + "mappedRoadName": "V Bokách III", + "label": "V Bokách III", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+748931502", + "mappedPosition": { + "latitude": 50.0798786, + "longitude": 14.4260037 + }, + "originalPosition": { + "latitude": 50.0799383, + "longitude": 14.4258216 + }, + "type": "stopOver", + "spot": 1.0, + "sideOfStreet": "left", + "mappedRoadName": "Štěpánská", + "label": "Štěpánská", + "shapeIndex": 116, + "source": "user" + } + ], + "mode": { + "type": "shortest", + "transportModes": [ + "publicTransportTimeTable" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "+565790671", + "mappedPosition": { + "latitude": 50.0378591, + "longitude": 14.3924721 + }, + "originalPosition": { + "latitude": 50.0377513, + "longitude": 14.3923344 + }, + "type": "stopOver", + "spot": 0.3, + "sideOfStreet": "left", + "mappedRoadName": "V Bokách III", + "label": "V Bokách III", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+748931502", + "mappedPosition": { + "latitude": 50.0798786, + "longitude": 14.4260037 + }, + "originalPosition": { + "latitude": 50.0799383, + "longitude": 14.4258216 + }, + "type": "stopOver", + "spot": 1.0, + "sideOfStreet": "left", + "mappedRoadName": "Štěpánská", + "label": "Štěpánská", + "shapeIndex": 116, + "source": "user" + }, + "length": 7835, + "travelTime": 2413, + "maneuver": [ + { + "position": { + "latitude": 50.0378591, + "longitude": 14.3924721 + }, + "instruction": "Head northwest on Kosořská. Go for 28 m.", + "travelTime": 32, + "length": 28, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0380039, + "longitude": 14.3921542 + }, + "instruction": "Turn left onto Kosořská. Go for 24 m.", + "travelTime": 24, + "length": 24, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0380039, + "longitude": 14.3918109 + }, + "instruction": "Take the street on the left, Slivenecká. Go for 343 m.", + "travelTime": 354, + "length": 343, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0376499, + "longitude": 14.3871975 + }, + "instruction": "Turn left onto Slivenecká. Go for 64 m.", + "travelTime": 72, + "length": 64, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0373602, + "longitude": 14.3879807 + }, + "instruction": "Turn right onto Slivenecká. Go for 91 m.", + "travelTime": 95, + "length": 91, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0365448, + "longitude": 14.3878305 + }, + "instruction": "Turn left onto K Barrandovu. Go for 124 m.", + "travelTime": 126, + "length": 124, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0363168, + "longitude": 14.3894618 + }, + "instruction": "Go to the Tram station Geologicka and take the rail 5 toward Ústřední dílny DP. Follow for 13 stations.", + "travelTime": 1440, + "length": 6911, + "id": "M7", + "stopName": "Geologicka", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 50.0800508, + "longitude": 14.423403 + }, + "instruction": "Get off at Vodickova.", + "travelTime": 0, + "length": 0, + "id": "M8", + "stopName": "Vodickova", + "nextRoadName": "Vodičkova", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 50.0800508, + "longitude": 14.423403 + }, + "instruction": "Head northeast on Vodičkova. Go for 65 m.", + "travelTime": 74, + "length": 65, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0804901, + "longitude": 14.4239759 + }, + "instruction": "Turn right onto V Jámě. Go for 163 m.", + "travelTime": 174, + "length": 163, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0796962, + "longitude": 14.4258857 + }, + "instruction": "Turn left onto Štěpánská. Go for 22 m.", + "travelTime": 22, + "length": 22, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0798786, + "longitude": 14.4260037 + }, + "instruction": "Arrive at Štěpánská. Your destination is on the left.", + "travelTime": 0, + "length": 0, + "id": "M12", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "publicTransportLine": [ + { + "lineName": "5", + "lineForeground": "#F5ADCE", + "lineBackground": "#F5ADCE", + "companyName": "HERE Technologies", + "destination": "Ústřední dílny DP", + "type": "railLight", + "id": "L1" + } + ], + "summary": { + "distance": 7835, + "baseTime": 2413, + "flags": [ + "noThroughRoad", + "builtUpArea" + ], + "text": "The trip takes 7.8 km and 40 mins.", + "travelTime": 2413, + "departure": "2019-09-21T17:16:17+02:00", + "timetableExpiration": "2019-09-21T00:00:00Z", + "_type": "PublicTransportRouteSummaryType" + } + } + ], + "language": "en-us", + "sourceAttribution": { + "attribution": "With the support of HERE Technologies. All information is provided without warranty of any kind.", + "supplier": [ + { + "title": "HERE Technologies", + "href": "https://transit.api.here.com/r?appId=Mt1bOYh3m9uxE7r3wuUx&u=https://wego.here.com" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/bike_response.json b/tests/fixtures/here_travel_time/bike_response.json new file mode 100644 index 00000000000000..a3af39129d01fe --- /dev/null +++ b/tests/fixtures/here_travel_time/bike_response.json @@ -0,0 +1,274 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-24T10:17:40Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201929-4522", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 87, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "bicycle" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 87, + "source": "user" + }, + "length": 12613, + "travelTime": 3292, + "maneuver": [ + { + "position": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "instruction": "Head south on Mannheim Rd (US-12/US-45). Go for 2.6 km.", + "travelTime": 646, + "length": 2648, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9579244, + "longitude": -87.8838551 + }, + "instruction": "Keep left onto Mannheim Rd (US-12/US-45). Go for 2.4 km.", + "travelTime": 621, + "length": 2427, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9364238, + "longitude": -87.8849387 + }, + "instruction": "Turn right onto W Belmont Ave. Go for 595 m.", + "travelTime": 158, + "length": 595, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9362521, + "longitude": -87.8921163 + }, + "instruction": "Turn left onto Cullerton St. Go for 669 m.", + "travelTime": 180, + "length": 669, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9305658, + "longitude": -87.8932428 + }, + "instruction": "Continue on N Landen Dr. Go for 976 m.", + "travelTime": 246, + "length": 976, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9217896, + "longitude": -87.8928781 + }, + "instruction": "Turn right onto E Fullerton Ave. Go for 904 m.", + "travelTime": 238, + "length": 904, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.921618, + "longitude": -87.9038107 + }, + "instruction": "Turn left onto N Wolf Rd. Go for 1.6 km.", + "travelTime": 417, + "length": 1604, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.907177, + "longitude": -87.9032314 + }, + "instruction": "Turn right onto W North Ave (IL-64). Go for 2.0 km.", + "travelTime": 574, + "length": 2031, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065225, + "longitude": -87.9277039 + }, + "instruction": "Turn left onto N Clinton Ave. Go for 275 m.", + "travelTime": 78, + "length": 275, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9040549, + "longitude": -87.9277253 + }, + "instruction": "Turn left onto E Third St. Go for 249 m.", + "travelTime": 63, + "length": 249, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9040334, + "longitude": -87.9247105 + }, + "instruction": "Continue on N Caroline Ave. Go for 96 m.", + "travelTime": 37, + "length": 96, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9038832, + "longitude": -87.9236054 + }, + "instruction": "Turn slightly left. Go for 113 m.", + "travelTime": 28, + "length": 113, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039047, + "longitude": -87.9222536 + }, + "instruction": "Turn left. Go for 26 m.", + "travelTime": 6, + "length": 26, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "instruction": "Arrive at your destination on the right.", + "travelTime": 0, + "length": 0, + "id": "M14", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 12613, + "baseTime": 3292, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park" + ], + "text": "The trip takes 12.6 km and 55 mins.", + "travelTime": 3292, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/car_enabled_response.json b/tests/fixtures/here_travel_time/car_enabled_response.json new file mode 100644 index 00000000000000..08da738f0464a9 --- /dev/null +++ b/tests/fixtures/here_travel_time/car_enabled_response.json @@ -0,0 +1,298 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T21:21:31Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1128310200", + "mappedPosition": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "originalPosition": { + "latitude": 38.9029809, + "longitude": -77.048338 + }, + "type": "stopOver", + "spot": 0.3538462, + "sideOfStreet": "right", + "mappedRoadName": "K St NW", + "label": "K St NW", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "-18459081", + "mappedPosition": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "originalPosition": { + "latitude": 39.042158, + "longitude": -77.119116 + }, + "type": "stopOver", + "spot": 0.7253521, + "sideOfStreet": "left", + "mappedRoadName": "Commonwealth Dr", + "label": "Commonwealth Dr", + "shapeIndex": 283, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "car" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1128310200", + "mappedPosition": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "originalPosition": { + "latitude": 38.9029809, + "longitude": -77.048338 + }, + "type": "stopOver", + "spot": 0.3538462, + "sideOfStreet": "right", + "mappedRoadName": "K St NW", + "label": "K St NW", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "-18459081", + "mappedPosition": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "originalPosition": { + "latitude": 39.042158, + "longitude": -77.119116 + }, + "type": "stopOver", + "spot": 0.7253521, + "sideOfStreet": "left", + "mappedRoadName": "Commonwealth Dr", + "label": "Commonwealth Dr", + "shapeIndex": 283, + "source": "user" + }, + "length": 23381, + "travelTime": 1817, + "maneuver": [ + { + "position": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "instruction": "Head toward 22nd St NW on K St NW. Go for 140 m.", + "travelTime": 36, + "length": 140, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9027703, + "longitude": -77.0494902 + }, + "instruction": "Take the 3rd exit from Washington Cir NW roundabout onto K St NW. Go for 325 m.", + "travelTime": 81, + "length": 325, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9026523, + "longitude": -77.0529449 + }, + "instruction": "Keep left onto K St NW (US-29). Go for 201 m.", + "travelTime": 29, + "length": 201, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9025235, + "longitude": -77.0552516 + }, + "instruction": "Keep right onto Whitehurst Fwy (US-29). Go for 1.4 km.", + "travelTime": 143, + "length": 1381, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9050448, + "longitude": -77.0701969 + }, + "instruction": "Turn left onto M St NW. Go for 784 m.", + "travelTime": 80, + "length": 784, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9060318, + "longitude": -77.0790696 + }, + "instruction": "Turn slightly left onto Canal Rd NW. Go for 4.2 km.", + "travelTime": 287, + "length": 4230, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9303219, + "longitude": -77.1117926 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 844 m.", + "travelTime": 55, + "length": 844, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9368558, + "longitude": -77.1166742 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 4.7 km.", + "travelTime": 294, + "length": 4652, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9706838, + "longitude": -77.1461463 + }, + "instruction": "Keep right onto Cabin John Pkwy N toward I-495 N. Go for 2.1 km.", + "travelTime": 90, + "length": 2069, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9858222, + "longitude": -77.1571326 + }, + "instruction": "Take left ramp onto I-495 N (Capital Beltway). Go for 2.9 km.", + "travelTime": 129, + "length": 2890, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0104449, + "longitude": -77.1508026 + }, + "instruction": "Keep left onto I-270-SPUR toward I-270/Rockville/Frederick. Go for 1.1 km.", + "travelTime": 48, + "length": 1136, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0192747, + "longitude": -77.144773 + }, + "instruction": "Take exit 1 toward Democracy Blvd/Old Georgetown Rd/MD-187 onto Democracy Blvd. Go for 1.8 km.", + "travelTime": 205, + "length": 1818, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0247464, + "longitude": -77.1253431 + }, + "instruction": "Turn left onto Old Georgetown Rd (MD-187). Go for 2.3 km.", + "travelTime": 230, + "length": 2340, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0447772, + "longitude": -77.1203649 + }, + "instruction": "Turn right onto Nicholson Ln. Go for 208 m.", + "travelTime": 31, + "length": 208, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0448952, + "longitude": -77.1179724 + }, + "instruction": "Turn right onto Commonwealth Dr. Go for 341 m.", + "travelTime": 75, + "length": 341, + "id": "M15", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "instruction": "Arrive at Commonwealth Dr. Your destination is on the left.", + "travelTime": 4, + "length": 22, + "id": "M16", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 23381, + "trafficTime": 1782, + "baseTime": 1712, + "flags": [ + "noThroughRoad", + "motorway", + "builtUpArea", + "park" + ], + "text": "The trip takes 23.4 km and 30 mins.", + "travelTime": 1782, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/car_response.json b/tests/fixtures/here_travel_time/car_response.json new file mode 100644 index 00000000000000..bda8454f3f3780 --- /dev/null +++ b/tests/fixtures/here_travel_time/car_response.json @@ -0,0 +1,299 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-19T07:38:39Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4446", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "+732182239", + "mappedPosition": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "originalPosition": { + "latitude": 38.9, + "longitude": -77.0483301 + }, + "type": "stopOver", + "spot": 0.4946237, + "sideOfStreet": "right", + "mappedRoadName": "22nd St NW", + "label": "22nd St NW", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+942865877", + "mappedPosition": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "originalPosition": { + "latitude": 38.9999999, + "longitude": -77.1000001 + }, + "type": "stopOver", + "spot": 1, + "sideOfStreet": "left", + "mappedRoadName": "Service Rd S", + "label": "Service Rd S", + "shapeIndex": 279, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "car" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "+732182239", + "mappedPosition": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "originalPosition": { + "latitude": 38.9, + "longitude": -77.0483301 + }, + "type": "stopOver", + "spot": 0.4946237, + "sideOfStreet": "right", + "mappedRoadName": "22nd St NW", + "label": "22nd St NW", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+942865877", + "mappedPosition": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "originalPosition": { + "latitude": 38.9999999, + "longitude": -77.1000001 + }, + "type": "stopOver", + "spot": 1, + "sideOfStreet": "left", + "mappedRoadName": "Service Rd S", + "label": "Service Rd S", + "shapeIndex": 279, + "source": "user" + }, + "length": 23903, + "travelTime": 1884, + "maneuver": [ + { + "position": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "instruction": "Head toward I St NW on 22nd St NW. Go for 279 m.", + "travelTime": 95, + "length": 279, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9021051, + "longitude": -77.048825 + }, + "instruction": "Turn left toward Pennsylvania Ave NW. Go for 71 m.", + "travelTime": 21, + "length": 71, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.902545, + "longitude": -77.0494151 + }, + "instruction": "Take the 3rd exit from Washington Cir NW roundabout onto K St NW. Go for 352 m.", + "travelTime": 90, + "length": 352, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9026523, + "longitude": -77.0529449 + }, + "instruction": "Keep left onto K St NW (US-29). Go for 201 m.", + "travelTime": 30, + "length": 201, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9025235, + "longitude": -77.0552516 + }, + "instruction": "Keep right onto Whitehurst Fwy (US-29). Go for 1.4 km.", + "travelTime": 131, + "length": 1381, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9050448, + "longitude": -77.0701969 + }, + "instruction": "Turn left onto M St NW. Go for 784 m.", + "travelTime": 78, + "length": 784, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9060318, + "longitude": -77.0790696 + }, + "instruction": "Turn slightly left onto Canal Rd NW. Go for 4.2 km.", + "travelTime": 277, + "length": 4230, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9303219, + "longitude": -77.1117926 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 844 m.", + "travelTime": 55, + "length": 844, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9368558, + "longitude": -77.1166742 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 4.7 km.", + "travelTime": 298, + "length": 4652, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9706838, + "longitude": -77.1461463 + }, + "instruction": "Keep right onto Cabin John Pkwy N toward I-495 N. Go for 2.1 km.", + "travelTime": 91, + "length": 2069, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9858222, + "longitude": -77.1571326 + }, + "instruction": "Take left ramp onto I-495 N (Capital Beltway). Go for 5.5 km.", + "travelTime": 238, + "length": 5538, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0153587, + "longitude": -77.1221781 + }, + "instruction": "Take exit 36 toward Bethesda onto MD-187 S (Old Georgetown Rd). Go for 2.4 km.", + "travelTime": 211, + "length": 2365, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9981818, + "longitude": -77.1093571 + }, + "instruction": "Turn left onto Lincoln Dr. Go for 506 m.", + "travelTime": 127, + "length": 506, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9987397, + "longitude": -77.1037138 + }, + "instruction": "Turn right onto Service Rd W. Go for 121 m.", + "travelTime": 36, + "length": 121, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9976454, + "longitude": -77.1036172 + }, + "instruction": "Turn left onto Service Rd S. Go for 510 m.", + "travelTime": 106, + "length": 510, + "id": "M15", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "instruction": "Arrive at Service Rd S. Your destination is on the left.", + "travelTime": 0, + "length": 0, + "id": "M16", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 23903, + "trafficTime": 1861, + "baseTime": 1803, + "flags": [ + "noThroughRoad", + "motorway", + "builtUpArea", + "park", + "privateRoad" + ], + "text": "The trip takes 23.9 km and 31 mins.", + "travelTime": 1861, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/car_shortest_response.json b/tests/fixtures/here_travel_time/car_shortest_response.json new file mode 100644 index 00000000000000..765c438c1cd14e --- /dev/null +++ b/tests/fixtures/here_travel_time/car_shortest_response.json @@ -0,0 +1,231 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T21:05:28Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1128310200", + "mappedPosition": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "originalPosition": { + "latitude": 38.9029809, + "longitude": -77.048338 + }, + "type": "stopOver", + "spot": 0.3538462, + "sideOfStreet": "right", + "mappedRoadName": "K St NW", + "label": "K St NW", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "-18459081", + "mappedPosition": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "originalPosition": { + "latitude": 39.042158, + "longitude": -77.119116 + }, + "type": "stopOver", + "spot": 0.7253521, + "sideOfStreet": "left", + "mappedRoadName": "Commonwealth Dr", + "label": "Commonwealth Dr", + "shapeIndex": 162, + "source": "user" + } + ], + "mode": { + "type": "shortest", + "transportModes": [ + "car" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1128310200", + "mappedPosition": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "originalPosition": { + "latitude": 38.9029809, + "longitude": -77.048338 + }, + "type": "stopOver", + "spot": 0.3538462, + "sideOfStreet": "right", + "mappedRoadName": "K St NW", + "label": "K St NW", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "-18459081", + "mappedPosition": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "originalPosition": { + "latitude": 39.042158, + "longitude": -77.119116 + }, + "type": "stopOver", + "spot": 0.7253521, + "sideOfStreet": "left", + "mappedRoadName": "Commonwealth Dr", + "label": "Commonwealth Dr", + "shapeIndex": 162, + "source": "user" + }, + "length": 18388, + "travelTime": 2493, + "maneuver": [ + { + "position": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "instruction": "Head west on K St NW. Go for 79 m.", + "travelTime": 22, + "length": 79, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9026523, + "longitude": -77.048825 + }, + "instruction": "Turn right onto 22nd St NW. Go for 141 m.", + "travelTime": 79, + "length": 141, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9039075, + "longitude": -77.048825 + }, + "instruction": "Keep left onto 22nd St NW. Go for 841 m.", + "travelTime": 256, + "length": 841, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9114928, + "longitude": -77.0487821 + }, + "instruction": "Turn left onto Massachusetts Ave NW. Go for 145 m.", + "travelTime": 22, + "length": 145, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9120293, + "longitude": -77.0502949 + }, + "instruction": "Take the 1st exit from Massachusetts Ave NW roundabout onto Massachusetts Ave NW. Go for 2.8 km.", + "travelTime": 301, + "length": 2773, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9286053, + "longitude": -77.073158 + }, + "instruction": "Turn right onto Wisconsin Ave NW. Go for 3.8 km.", + "travelTime": 610, + "length": 3801, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9607918, + "longitude": -77.0857322 + }, + "instruction": "Continue on Wisconsin Ave (MD-355). Go for 9.7 km.", + "travelTime": 1013, + "length": 9686, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0447664, + "longitude": -77.1116638 + }, + "instruction": "Turn left onto Nicholson Ln. Go for 559 m.", + "travelTime": 111, + "length": 559, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0448952, + "longitude": -77.1179724 + }, + "instruction": "Turn left onto Commonwealth Dr. Go for 341 m.", + "travelTime": 75, + "length": 341, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "instruction": "Arrive at Commonwealth Dr. Your destination is on the left.", + "travelTime": 4, + "length": 22, + "id": "M10", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 18388, + "trafficTime": 2427, + "baseTime": 2150, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park" + ], + "text": "The trip takes 18.4 km and 40 mins.", + "travelTime": 2427, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/pedestrian_response.json b/tests/fixtures/here_travel_time/pedestrian_response.json new file mode 100644 index 00000000000000..07881e8bd3d06b --- /dev/null +++ b/tests/fixtures/here_travel_time/pedestrian_response.json @@ -0,0 +1,308 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T18:40:10Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 122, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "pedestrian" + ], + "trafficMode": "disabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 122, + "source": "user" + }, + "length": 12533, + "travelTime": 12631, + "maneuver": [ + { + "position": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "instruction": "Head south on Mannheim Rd. Go for 848 m.", + "travelTime": 848, + "length": 848, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9722581, + "longitude": -87.8776109 + }, + "instruction": "Take the street on the left, Mannheim Rd. Go for 4.2 km.", + "travelTime": 4239, + "length": 4227, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9364238, + "longitude": -87.8849387 + }, + "instruction": "Turn right onto W Belmont Ave. Go for 595 m.", + "travelTime": 605, + "length": 595, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9362521, + "longitude": -87.8921163 + }, + "instruction": "Turn left onto Cullerton St. Go for 406 m.", + "travelTime": 411, + "length": 406, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9326043, + "longitude": -87.8919983 + }, + "instruction": "Turn right onto Cullerton St. Go for 1.2 km.", + "travelTime": 1249, + "length": 1239, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9217896, + "longitude": -87.8928781 + }, + "instruction": "Turn right onto E Fullerton Ave. Go for 786 m.", + "travelTime": 796, + "length": 786, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9216394, + "longitude": -87.9023838 + }, + "instruction": "Turn left onto La Porte Ave. Go for 424 m.", + "travelTime": 430, + "length": 424, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9180024, + "longitude": -87.9028559 + }, + "instruction": "Turn right onto E Palmer Ave. Go for 864 m.", + "travelTime": 875, + "length": 864, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9175196, + "longitude": -87.9132199 + }, + "instruction": "Turn left onto N Railroad Ave. Go for 1.2 km.", + "travelTime": 1180, + "length": 1170, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9070268, + "longitude": -87.9130161 + }, + "instruction": "Turn right onto W North Ave. Go for 638 m.", + "travelTime": 638, + "length": 638, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9068551, + "longitude": -87.9207087 + }, + "instruction": "Take the street on the left, E North Ave. Go for 354 m.", + "travelTime": 354, + "length": 354, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065869, + "longitude": -87.9249573 + }, + "instruction": "Take the street on the left, E North Ave. Go for 228 m.", + "travelTime": 242, + "length": 228, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065225, + "longitude": -87.9277039 + }, + "instruction": "Turn left. Go for 409 m.", + "travelTime": 419, + "length": 409, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9040334, + "longitude": -87.9260409 + }, + "instruction": "Turn left onto E Third St. Go for 206 m.", + "travelTime": 206, + "length": 206, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9038832, + "longitude": -87.9236054 + }, + "instruction": "Turn left. Go for 113 m.", + "travelTime": 113, + "length": 113, + "id": "M15", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039047, + "longitude": -87.9222536 + }, + "instruction": "Turn left. Go for 26 m.", + "travelTime": 26, + "length": 26, + "id": "M16", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "instruction": "Arrive at your destination on the right.", + "travelTime": 0, + "length": 0, + "id": "M17", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 12533, + "baseTime": 12631, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park", + "privateRoad" + ], + "text": "The trip takes 12.5 km and 3:31 h.", + "travelTime": 12631, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/public_response.json b/tests/fixtures/here_travel_time/public_response.json new file mode 100644 index 00000000000000..149b4d06c3975e --- /dev/null +++ b/tests/fixtures/here_travel_time/public_response.json @@ -0,0 +1,294 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T18:40:37Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 191, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "publicTransport" + ], + "trafficMode": "disabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 191, + "source": "user" + }, + "length": 22325, + "travelTime": 5350, + "maneuver": [ + { + "position": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "instruction": "Head south on Mannheim Rd. Go for 848 m.", + "travelTime": 848, + "length": 848, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9722581, + "longitude": -87.8776109 + }, + "instruction": "Take the street on the left, Mannheim Rd. Go for 825 m.", + "travelTime": 825, + "length": 825, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9650483, + "longitude": -87.8769565 + }, + "instruction": "Go to the stop Mannheim/Lawrence and take the bus 332 toward Palmer/Schiller. Follow for 7 stops.", + "travelTime": 475, + "length": 4360, + "id": "M3", + "stopName": "Mannheim/Lawrence", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9541478, + "longitude": -87.9133594 + }, + "instruction": "Get off at Irving Park/Taft.", + "travelTime": 0, + "length": 0, + "id": "M4", + "stopName": "Irving Park/Taft", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9541478, + "longitude": -87.9133594 + }, + "instruction": "Take the bus 332 toward Cargo Rd./Delta Cargo. Follow for 1 stop.", + "travelTime": 155, + "length": 3505, + "id": "M5", + "stopName": "Irving Park/Taft", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9599199, + "longitude": -87.9162776 + }, + "instruction": "Get off at Cargo Rd./S. Access Rd./Lufthansa.", + "travelTime": 0, + "length": 0, + "id": "M6", + "stopName": "Cargo Rd./S. Access Rd./Lufthansa", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9599199, + "longitude": -87.9162776 + }, + "instruction": "Take the bus 332 toward Palmer/Schiller. Follow for 41 stops.", + "travelTime": 1510, + "length": 11261, + "id": "M7", + "stopName": "Cargo Rd./S. Access Rd./Lufthansa", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9041729, + "longitude": -87.9399669 + }, + "instruction": "Get off at York/Third.", + "travelTime": 0, + "length": 0, + "id": "M8", + "stopName": "York/Third", + "nextRoadName": "N York St", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9041729, + "longitude": -87.9399669 + }, + "instruction": "Head east on N York St. Go for 33 m.", + "travelTime": 43, + "length": 33, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039476, + "longitude": -87.9398811 + }, + "instruction": "Turn left onto E Third St. Go for 1.4 km.", + "travelTime": 1355, + "length": 1354, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9038832, + "longitude": -87.9236054 + }, + "instruction": "Turn left. Go for 113 m.", + "travelTime": 113, + "length": 113, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039047, + "longitude": -87.9222536 + }, + "instruction": "Turn left. Go for 26 m.", + "travelTime": 26, + "length": 26, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "instruction": "Arrive at your destination on the right.", + "travelTime": 0, + "length": 0, + "id": "M13", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "publicTransportLine": [ + { + "lineName": "332", + "companyName": "", + "destination": "Palmer/Schiller", + "type": "busPublic", + "id": "L1" + }, + { + "lineName": "332", + "companyName": "", + "destination": "Cargo Rd./Delta Cargo", + "type": "busPublic", + "id": "L2" + }, + { + "lineName": "332", + "companyName": "", + "destination": "Palmer/Schiller", + "type": "busPublic", + "id": "L3" + } + ], + "summary": { + "distance": 22325, + "baseTime": 5350, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park" + ], + "text": "The trip takes 22.3 km and 1:29 h.", + "travelTime": 5350, + "departure": "1970-01-01T00:00:00Z", + "_type": "PublicTransportRouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/public_time_table_response.json b/tests/fixtures/here_travel_time/public_time_table_response.json new file mode 100644 index 00000000000000..52df0d4eb35ad5 --- /dev/null +++ b/tests/fixtures/here_travel_time/public_time_table_response.json @@ -0,0 +1,308 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-08-06T06:43:24Z", + "mapVersion": "8.30.99.152", + "moduleVersion": "7.2.201931-4739", + "interfaceVersion": "2.6.66", + "availableMapVersion": [ + "8.30.99.152" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 111, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "publicTransportTimeTable" + ], + "trafficMode": "disabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 111, + "source": "user" + }, + "length": 14775, + "travelTime": 4784, + "maneuver": [ + { + "position": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "instruction": "Head south on Mannheim Rd. Go for 848 m.", + "travelTime": 848, + "length": 848, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9722581, + "longitude": -87.8776109 + }, + "instruction": "Take the street on the left, Mannheim Rd. Go for 812 m.", + "travelTime": 812, + "length": 812, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.965051, + "longitude": -87.8769591 + }, + "instruction": "Go to the Bus stop Mannheim/Lawrence and take the bus 330 toward Archer/Harlem (Terminal). Follow for 33 stops.", + "travelTime": 900, + "length": 7815, + "id": "M3", + "stopName": "Mannheim/Lawrence", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.896836, + "longitude": -87.883771 + }, + "instruction": "Get off at Mannheim/Lake.", + "travelTime": 0, + "length": 0, + "id": "M4", + "stopName": "Mannheim/Lake", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.896836, + "longitude": -87.883771 + }, + "instruction": "Walk to Bus Lake/Mannheim.", + "travelTime": 300, + "length": 72, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.897263, + "longitude": -87.8842648 + }, + "instruction": "Take the bus 309 toward Elmhurst Metra Station. Follow for 18 stops.", + "travelTime": 1020, + "length": 4362, + "id": "M6", + "stopName": "Lake/Mannheim", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9066347, + "longitude": -87.928671 + }, + "instruction": "Get off at North/Berteau.", + "travelTime": 0, + "length": 0, + "id": "M7", + "stopName": "North/Berteau", + "nextRoadName": "E Berteau Ave", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9066347, + "longitude": -87.928671 + }, + "instruction": "Head north on E Berteau Ave. Go for 23 m.", + "travelTime": 40, + "length": 23, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9067693, + "longitude": -87.9284549 + }, + "instruction": "Turn right onto E Berteau Ave. Go for 40 m.", + "travelTime": 44, + "length": 40, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065011, + "longitude": -87.9282939 + }, + "instruction": "Turn left onto E North Ave. Go for 49 m.", + "travelTime": 56, + "length": 49, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065225, + "longitude": -87.9277039 + }, + "instruction": "Turn slightly right. Go for 409 m.", + "travelTime": 419, + "length": 409, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9040334, + "longitude": -87.9260409 + }, + "instruction": "Turn left onto E Third St. Go for 206 m.", + "travelTime": 206, + "length": 206, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9038832, + "longitude": -87.9236054 + }, + "instruction": "Turn left. Go for 113 m.", + "travelTime": 113, + "length": 113, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039047, + "longitude": -87.9222536 + }, + "instruction": "Turn left. Go for 26 m.", + "travelTime": 26, + "length": 26, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "instruction": "Arrive at your destination on the right.", + "travelTime": 0, + "length": 0, + "id": "M15", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "publicTransportLine": [ + { + "lineName": "330", + "companyName": "PACE", + "destination": "Archer/Harlem (Terminal)", + "type": "busPublic", + "id": "L1" + }, + { + "lineName": "309", + "companyName": "PACE", + "destination": "Elmhurst Metra Station", + "type": "busPublic", + "id": "L2" + } + ], + "summary": { + "distance": 14775, + "baseTime": 4784, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park" + ], + "text": "The trip takes 14.8 km and 1:20 h.", + "travelTime": 4784, + "departure": "2019-08-06T05:09:20-05:00", + "timetableExpiration": "2019-08-04T00:00:00Z", + "_type": "PublicTransportRouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json b/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json new file mode 100644 index 00000000000000..81fb246178c938 --- /dev/null +++ b/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json @@ -0,0 +1,15 @@ +{ + "_type": "ns2:RoutingServiceErrorType", + "type": "PermissionError", + "subtype": "InvalidCredentials", + "details": "This is not a valid app_id and app_code pair. Please verify that the values are not swapped between the app_id and app_code and the values provisioned by HERE (either by your customer representative or via http://developer.here.com/myapps) were copied correctly into the request.", + "metaInfo": { + "timestamp": "2019-07-10T09:43:14Z", + "mapVersion": "8.30.98.152", + "moduleVersion": "7.2.201927-4307", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.152" + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/routing_error_no_route_found.json b/tests/fixtures/here_travel_time/routing_error_no_route_found.json new file mode 100644 index 00000000000000..a776fa91c43b2a --- /dev/null +++ b/tests/fixtures/here_travel_time/routing_error_no_route_found.json @@ -0,0 +1,21 @@ +{ + "_type": "ns2:RoutingServiceErrorType", + "type": "ApplicationError", + "subtype": "NoRouteFound", + "details": "Error is NGEO_ERROR_ROUTE_NO_END_POINT", + "additionalData": [ + { + "key": "error_code", + "value": "NGEO_ERROR_ROUTE_NO_END_POINT" + } + ], + "metaInfo": { + "timestamp": "2019-07-10T09:51:04Z", + "mapVersion": "8.30.98.152", + "moduleVersion": "7.2.201927-4307", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.152" + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/truck_response.json b/tests/fixtures/here_travel_time/truck_response.json new file mode 100644 index 00000000000000..a302d564902e81 --- /dev/null +++ b/tests/fixtures/here_travel_time/truck_response.json @@ -0,0 +1,187 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T14:25:00Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "+930461269", + "mappedPosition": { + "latitude": 41.9800687, + "longitude": -87.8805614 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5555556, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "-1035319462", + "mappedPosition": { + "latitude": 41.9042909, + "longitude": -87.9216528 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0, + "sideOfStreet": "left", + "mappedRoadName": "Eisenhower Expy E", + "label": "Eisenhower Expy E - I-290", + "shapeIndex": 135, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "truck" + ], + "trafficMode": "disabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "+930461269", + "mappedPosition": { + "latitude": 41.9800687, + "longitude": -87.8805614 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5555556, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "-1035319462", + "mappedPosition": { + "latitude": 41.9042909, + "longitude": -87.9216528 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0, + "sideOfStreet": "left", + "mappedRoadName": "Eisenhower Expy E", + "label": "Eisenhower Expy E - I-290", + "shapeIndex": 135, + "source": "user" + }, + "length": 13049, + "travelTime": 812, + "maneuver": [ + { + "position": { + "latitude": 41.9800687, + "longitude": -87.8805614 + }, + "instruction": "Take ramp onto I-190. Go for 631 m.", + "travelTime": 53, + "length": 631, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.98259, + "longitude": -87.8744352 + }, + "instruction": "Take exit 1D toward Indiana onto I-294 S (Tri-State Tollway). Go for 10.9 km.", + "travelTime": 573, + "length": 10872, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9059324, + "longitude": -87.9199362 + }, + "instruction": "Take exit 33 toward Rockford/US-20/IL-64 onto I-290 W (Eisenhower Expy W). Go for 475 m.", + "travelTime": 54, + "length": 475, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9067156, + "longitude": -87.9237771 + }, + "instruction": "Take exit 13B toward North Ave onto IL-64 W (E North Ave). Go for 435 m.", + "travelTime": 51, + "length": 435, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065869, + "longitude": -87.9249573 + }, + "instruction": "Take ramp onto I-290 E (Eisenhower Expy E) toward Chicago/I-294 S. Go for 636 m.", + "travelTime": 81, + "length": 636, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9042909, + "longitude": -87.9216528 + }, + "instruction": "Arrive at Eisenhower Expy E (I-290). Your destination is on the left.", + "travelTime": 0, + "length": 0, + "id": "M6", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 13049, + "trafficTime": 812, + "baseTime": 812, + "flags": [ + "tollroad", + "motorway", + "builtUpArea" + ], + "text": "The trip takes 13.0 km and 14 mins.", + "travelTime": 812, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file From 9fd9ccc2a322dfd973e16165a61e972dccdaffc8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 12:13:12 +0200 Subject: [PATCH 124/296] Remove deprecated srp_energy integration (ADR-0004) (#26826) --- .coveragerc | 1 - .../components/srp_energy/__init__.py | 1 - .../components/srp_energy/manifest.json | 10 -- homeassistant/components/srp_energy/sensor.py | 156 ------------------ requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/srp_energy/__init__.py | 1 - tests/components/srp_energy/test_sensor.py | 62 ------- 8 files changed, 237 deletions(-) delete mode 100644 homeassistant/components/srp_energy/__init__.py delete mode 100644 homeassistant/components/srp_energy/manifest.json delete mode 100644 homeassistant/components/srp_energy/sensor.py delete mode 100644 tests/components/srp_energy/__init__.py delete mode 100644 tests/components/srp_energy/test_sensor.py diff --git a/.coveragerc b/.coveragerc index b6bdcc2704372f..256a6b3e41eb6d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -608,7 +608,6 @@ omit = homeassistant/components/spotcrime/sensor.py homeassistant/components/spotify/media_player.py homeassistant/components/squeezebox/media_player.py - homeassistant/components/srp_energy/sensor.py homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py deleted file mode 100644 index 71e04d7b8c99aa..00000000000000 --- a/homeassistant/components/srp_energy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The srp_energy component.""" diff --git a/homeassistant/components/srp_energy/manifest.json b/homeassistant/components/srp_energy/manifest.json deleted file mode 100644 index 050a78223c17f5..00000000000000 --- a/homeassistant/components/srp_energy/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "srp_energy", - "name": "Srp energy", - "documentation": "https://www.home-assistant.io/components/srp_energy", - "requirements": [ - "srpenergy==1.0.6" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py deleted file mode 100644 index f1d1787b7b48c5..00000000000000 --- a/homeassistant/components/srp_energy/sensor.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Platform for retrieving energy data from SRP.""" -from datetime import datetime, timedelta -import logging - -from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout -import voluptuous as vol - -from homeassistant.const import ( - CONF_NAME, - CONF_PASSWORD, - ENERGY_KILO_WATT_HOUR, - CONF_USERNAME, - CONF_ID, -) -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) - -ATTRIBUTION = "Powered by SRP Energy" - -DEFAULT_NAME = "SRP Energy" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1440) -ENERGY_KWH = ENERGY_KILO_WATT_HOUR - -ATTR_READING_COST = "reading_cost" -ATTR_READING_TIME = "datetime" -ATTR_READING_USAGE = "reading_usage" -ATTR_DAILY_USAGE = "daily_usage" -ATTR_USAGE_HISTORY = "usage_history" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_ID): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the SRP energy.""" - _LOGGER.warning( - "The srp_energy integration is deprecated and will be removed " - "in Home Assistant 0.100.0. For more information see ADR-0004:" - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - name = config[CONF_NAME] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - account_id = config[CONF_ID] - - from srpenergy.client import SrpEnergyClient - - srp_client = SrpEnergyClient(account_id, username, password) - - if not srp_client.validate(): - _LOGGER.error("Couldn't connect to %s. Check credentials", name) - return - - add_entities([SrpEnergy(name, srp_client)], True) - - -class SrpEnergy(Entity): - """Representation of an srp usage.""" - - def __init__(self, name, client): - """Initialize SRP Usage.""" - self._state = None - self._name = name - self._client = client - self._history = None - self._usage = None - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION - - @property - def state(self): - """Return the current state.""" - if self._state is None: - return None - - return f"{self._state:.2f}" - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return ENERGY_KWH - - @property - def history(self): - """Return the energy usage history of this entity, if any.""" - if self._usage is None: - return None - - history = [ - { - ATTR_READING_TIME: isodate, - ATTR_READING_USAGE: kwh, - ATTR_READING_COST: cost, - } - for _, _, isodate, kwh, cost in self._usage - ] - - return history - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attributes = {ATTR_USAGE_HISTORY: self.history} - - return attributes - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest usage from SRP Energy.""" - start_date = datetime.now() + timedelta(days=-1) - end_date = datetime.now() - - try: - - usage = self._client.usage(start_date, end_date) - - daily_usage = 0.0 - for _, _, _, kwh, _ in usage: - daily_usage += float(kwh) - - if usage: - - self._state = daily_usage - self._usage = usage - - else: - _LOGGER.error("Unable to fetch data from SRP. No data") - - except (ConnectError, HTTPError, Timeout) as error: - _LOGGER.error("Unable to connect to SRP. %s", error) - except ValueError as error: - _LOGGER.error("Value error connecting to SRP. %s", error) - except TypeError as error: - _LOGGER.error( - "Type error connecting to SRP. " "Check username and password. %s", - error, - ) diff --git a/requirements_all.txt b/requirements_all.txt index bf217688d266e6..011e8b7a096100 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1814,9 +1814,6 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.sql sqlalchemy==1.3.8 -# homeassistant.components.srp_energy -srpenergy==1.0.6 - # homeassistant.components.starlingbank starlingbank==3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e846c9c416ac1..c0e94a5afe58e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -407,9 +407,6 @@ somecomfort==0.5.2 # homeassistant.components.sql sqlalchemy==1.3.8 -# homeassistant.components.srp_energy -srpenergy==1.0.6 - # homeassistant.components.statsd statsd==3.2.1 diff --git a/tests/components/srp_energy/__init__.py b/tests/components/srp_energy/__init__.py deleted file mode 100644 index 2a278cf1d38de2..00000000000000 --- a/tests/components/srp_energy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the srp_energy component.""" diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py deleted file mode 100644 index e33902c3fd8c6e..00000000000000 --- a/tests/components/srp_energy/test_sensor.py +++ /dev/null @@ -1,62 +0,0 @@ -"""The tests for the Srp Energy Platform.""" -from unittest.mock import patch -import logging -from homeassistant.setup import async_setup_component - -_LOGGER = logging.getLogger(__name__) - -VALID_CONFIG_MINIMAL = { - "sensor": { - "platform": "srp_energy", - "username": "foo", - "password": "bar", - "id": 1234, - } -} - -PATCH_INIT = "srpenergy.client.SrpEnergyClient.__init__" -PATCH_VALIDATE = "srpenergy.client.SrpEnergyClient.validate" -PATCH_USAGE = "srpenergy.client.SrpEnergyClient.usage" - - -def mock_usage(self, startdate, enddate): # pylint: disable=invalid-name - """Mock srpusage usage.""" - _LOGGER.log(logging.INFO, "Calling mock usage") - usage = [ - ("9/19/2018", "12:00 AM", "2018-09-19T00:00:00-7:00", "1.2", "0.17"), - ("9/19/2018", "1:00 AM", "2018-09-19T01:00:00-7:00", "2.1", "0.30"), - ("9/19/2018", "2:00 AM", "2018-09-19T02:00:00-7:00", "1.5", "0.23"), - ("9/19/2018", "9:00 PM", "2018-09-19T21:00:00-7:00", "1.2", "0.19"), - ("9/19/2018", "10:00 PM", "2018-09-19T22:00:00-7:00", "1.1", "0.18"), - ("9/19/2018", "11:00 PM", "2018-09-19T23:00:00-7:00", "0.4", "0.09"), - ] - return usage - - -async def test_setup_with_config(hass): - """Test the platform setup with configuration.""" - with patch(PATCH_INIT, return_value=None), patch( - PATCH_VALIDATE, return_value=True - ), patch(PATCH_USAGE, new=mock_usage): - - await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL) - - state = hass.states.get("sensor.srp_energy") - assert state is not None - - -async def test_daily_usage(hass): - """Test the platform daily usage.""" - with patch(PATCH_INIT, return_value=None), patch( - PATCH_VALIDATE, return_value=True - ), patch(PATCH_USAGE, new=mock_usage): - - await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL) - - state = hass.states.get("sensor.srp_energy") - - assert state - assert state.state == "7.50" - - assert state.attributes - assert state.attributes.get("unit_of_measurement") From ad9daa922b7826bb40cce144c0d4a348378c7a95 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 12:16:41 +0200 Subject: [PATCH 125/296] Remove deprecated fedex integration (ADR-0004) (#26822) --- .coveragerc | 1 - homeassistant/components/fedex/__init__.py | 1 - homeassistant/components/fedex/manifest.json | 10 -- homeassistant/components/fedex/sensor.py | 120 ------------------- requirements_all.txt | 3 - 5 files changed, 135 deletions(-) delete mode 100644 homeassistant/components/fedex/__init__.py delete mode 100644 homeassistant/components/fedex/manifest.json delete mode 100644 homeassistant/components/fedex/sensor.py diff --git a/.coveragerc b/.coveragerc index 256a6b3e41eb6d..9fe3e10c8bc27a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -198,7 +198,6 @@ omit = homeassistant/components/evohome/* homeassistant/components/familyhub/camera.py homeassistant/components/fastdotcom/* - homeassistant/components/fedex/sensor.py homeassistant/components/ffmpeg/camera.py homeassistant/components/fibaro/* homeassistant/components/filesize/sensor.py diff --git a/homeassistant/components/fedex/__init__.py b/homeassistant/components/fedex/__init__.py deleted file mode 100644 index d685ab50372de5..00000000000000 --- a/homeassistant/components/fedex/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The fedex component.""" diff --git a/homeassistant/components/fedex/manifest.json b/homeassistant/components/fedex/manifest.json deleted file mode 100644 index b34a8b8383ef85..00000000000000 --- a/homeassistant/components/fedex/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "fedex", - "name": "Fedex", - "documentation": "https://www.home-assistant.io/components/fedex", - "requirements": [ - "fedexdeliverymanager==1.0.6" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/fedex/sensor.py b/homeassistant/components/fedex/sensor.py deleted file mode 100644 index 2f499e52e234e5..00000000000000 --- a/homeassistant/components/fedex/sensor.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Sensor for Fedex packages.""" -import logging -from collections import defaultdict -from datetime import timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, - CONF_USERNAME, - CONF_PASSWORD, - ATTR_ATTRIBUTION, - CONF_SCAN_INTERVAL, -) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -from homeassistant.util import slugify -from homeassistant.util.dt import now, parse_date - -_LOGGER = logging.getLogger(__name__) - -COOKIE = "fedexdeliverymanager_cookies.pickle" - -DOMAIN = "fedex" - -ICON = "mdi:package-variant-closed" - -STATUS_DELIVERED = "delivered" - -SCAN_INTERVAL = timedelta(seconds=1800) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Fedex platform.""" - import fedexdeliverymanager - - _LOGGER.warning( - "The fedex integration is deprecated and will be removed " - "in Home Assistant 0.100.0. For more information see ADR-0004:" - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - name = config.get(CONF_NAME) - update_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - - try: - cookie = hass.config.path(COOKIE) - session = fedexdeliverymanager.get_session( - config.get(CONF_USERNAME), config.get(CONF_PASSWORD), cookie_path=cookie - ) - except fedexdeliverymanager.FedexError: - _LOGGER.exception("Could not connect to Fedex Delivery Manager") - return False - - add_entities([FedexSensor(session, name, update_interval)], True) - - -class FedexSensor(Entity): - """Fedex Sensor.""" - - def __init__(self, session, name, interval): - """Initialize the sensor.""" - self._session = session - self._name = name - self._attributes = None - self._state = None - self.update = Throttle(interval)(self._update) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name or DOMAIN - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "packages" - - def _update(self): - """Update device state.""" - import fedexdeliverymanager - - status_counts = defaultdict(int) - for package in fedexdeliverymanager.get_packages(self._session): - status = slugify(package["primary_status"]) - skip = ( - status == STATUS_DELIVERED - and parse_date(package["delivery_date"]) < now().date() - ) - if skip: - continue - status_counts[status] += 1 - self._attributes = {ATTR_ATTRIBUTION: fedexdeliverymanager.ATTRIBUTION} - self._attributes.update(status_counts) - self._state = sum(status_counts.values()) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @property - def icon(self): - """Icon to use in the frontend.""" - return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 011e8b7a096100..3214c5e43ab38a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -478,9 +478,6 @@ evohome-async==0.3.3b4 # homeassistant.components.fastdotcom fastdotcom==0.0.3 -# homeassistant.components.fedex -fedexdeliverymanager==1.0.6 - # homeassistant.components.feedreader feedparser-homeassistant==5.2.2.dev1 From 2c80ce3195e4e8d20e0fd40c918a9881dea84506 Mon Sep 17 00:00:00 2001 From: MajestyIV Date: Mon, 23 Sep 2019 14:39:10 +0200 Subject: [PATCH 126/296] HM-CC-TC was not recognized (#26623) * HM-CC-TC was not recognized * guard instead of exception --- homeassistant/components/homematic/climate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 1a2f642f91ccc5..935ebb9b497620 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -94,7 +94,9 @@ def preset_mode(self): if self._data.get("BOOST_MODE", False): return "boost" - # Get the name of the mode + if not self._hm_control_mode: + return None + mode = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][self._hm_control_mode] mode = mode.lower() @@ -177,8 +179,9 @@ def _hm_control_mode(self): """Return Control mode.""" if HMIP_CONTROL_MODE in self._data: return self._data[HMIP_CONTROL_MODE] + # Homematic - return self._data["CONTROL_MODE"] + return self._data.get("CONTROL_MODE") def _init_data_struct(self): """Generate a data dict (self._data) from the Homematic metadata.""" From 61634d0a645f939b46772ca43a18f723287918f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 23 Sep 2019 14:08:44 +0100 Subject: [PATCH 127/296] Store ZHA light brightness when fading off to turn on at the correct brightness (#26680) * Use light's on_level in ZHA to turn on at the correct brightness Previously, if the light is turned off with a time transition, the brightness level stored in the light will be 1. The next time the light is turned on with no explicit brightness, it will be at 1. This is solved by storing the current brightness in on_level before turning off, and then using that when turning on (by calling the onOff cluster 'on' command). * store off light level locally to avoid wearing device's flash memory * store off brightness in HA attributes * improve set/clear of off_brightness * fix device_state_attributes; clear off_brightness when light goes on * fix tests --- homeassistant/components/zha/light.py | 25 ++++++++++++++++++++----- tests/components/zha/test_light.py | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index c2273c54073ac0..fb388afac0ffc9 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -25,8 +25,6 @@ _LOGGER = logging.getLogger(__name__) -DEFAULT_DURATION = 5 - CAPABILITIES_COLOR_LOOP = 0x4 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 @@ -91,6 +89,7 @@ def __init__(self, unique_id, zha_device, channels, **kwargs): self._color_temp = None self._hs_color = None self._brightness = None + self._off_brightness = None self._effect_list = [] self._effect = None self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) @@ -130,7 +129,8 @@ def brightness(self): @property def device_state_attributes(self): """Return state attributes.""" - return self.state_attributes + attributes = {"off_brightness": self._off_brightness} + return attributes def set_level(self, value): """Set the brightness of this light between 0..254. @@ -171,6 +171,8 @@ def supported_features(self): def async_set_state(self, state): """Set the state.""" self._state = bool(state) + if state: + self._off_brightness = None self.async_schedule_update_ha_state() async def async_added_to_hass(self): @@ -191,6 +193,8 @@ def async_restore_last_state(self, last_state): self._state = last_state.state == STATE_ON if "brightness" in last_state.attributes: self._brightness = last_state.attributes["brightness"] + if "off_brightness" in last_state.attributes: + self._off_brightness = last_state.attributes["off_brightness"] if "color_temp" in last_state.attributes: self._color_temp = last_state.attributes["color_temp"] if "hs_color" in last_state.attributes: @@ -201,10 +205,13 @@ def async_restore_last_state(self, last_state): async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) - duration = transition * 10 if transition is not None else DEFAULT_DURATION + duration = transition * 10 if transition else 0 brightness = kwargs.get(light.ATTR_BRIGHTNESS) effect = kwargs.get(light.ATTR_EFFECT) + if brightness is None and self._off_brightness is not None: + brightness = self._off_brightness + t_log = {} if ( brightness is not None or transition @@ -225,13 +232,14 @@ async def async_turn_on(self, **kwargs): self._brightness = level if brightness is None or brightness: + # since some lights don't always turn on with move_to_level_with_on_off, + # we should call the on command on the on_off cluster if brightness is not 0. result = await self._on_off_channel.on() t_log["on_off"] = result if not isinstance(result, list) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = True - if ( light.ATTR_COLOR_TEMP in kwargs and self.supported_features & light.SUPPORT_COLOR_TEMP @@ -289,6 +297,7 @@ async def async_turn_on(self, **kwargs): t_log["color_loop_set"] = result self._effect = None + self._off_brightness = None self.debug("turned on: %s", t_log) self.async_schedule_update_ha_state() @@ -296,6 +305,7 @@ async def async_turn_off(self, **kwargs): """Turn the entity off.""" duration = kwargs.get(light.ATTR_TRANSITION) supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS + if duration and supports_level: result = await self._level_channel.move_to_level_with_on_off( 0, duration * 10 @@ -306,6 +316,11 @@ async def async_turn_off(self, **kwargs): if not isinstance(result, list) or result[1] is not Status.SUCCESS: return self._state = False + + if duration and supports_level: + # store current brightness so that the next turn_on uses it. + self._off_brightness = self._brightness + self.async_schedule_update_ha_state() async def async_update(self): diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 2b167d52ac65d1..3101abc52649fa 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -230,7 +230,7 @@ async def async_test_level_on_off_from_hass( 4, (types.uint8_t, types.uint16_t), 10, - 5.0, + 0, expect_reply=True, manufacturer=None, ) From 8a9e47d3c7b95d00af6ca9b8be596c4601eef9e7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 15:43:48 +0200 Subject: [PATCH 128/296] Bump pyotp to 2.3.0 (#26849) --- homeassistant/auth/mfa_modules/notify.py | 2 +- homeassistant/auth/mfa_modules/totp.py | 2 +- homeassistant/components/otp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index a6a754fc2a6f3f..01c5c12efb7588 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -22,7 +22,7 @@ SetupFlow, ) -REQUIREMENTS = ["pyotp==2.2.7"] +REQUIREMENTS = ["pyotp==2.3.0"] CONF_MESSAGE = "message" diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index d6d901ac3b1fe5..4e417fca2198b1 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -16,7 +16,7 @@ SetupFlow, ) -REQUIREMENTS = ["pyotp==2.2.7", "PyQRCode==1.2.1"] +REQUIREMENTS = ["pyotp==2.3.0", "PyQRCode==1.2.1"] CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA) diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index cea246af328d26..112fca24194a8e 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -3,7 +3,7 @@ "name": "Otp", "documentation": "https://www.home-assistant.io/components/otp", "requirements": [ - "pyotp==2.2.7" + "pyotp==2.3.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 3214c5e43ab38a..07dd2c3965193c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1367,7 +1367,7 @@ pyotgw==0.4b4 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp -pyotp==2.2.7 +pyotp==2.3.0 # homeassistant.components.owlet pyowlet==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0e94a5afe58e6..7ece1a030aae6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ pyopenuv==1.0.9 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp -pyotp==2.2.7 +pyotp==2.3.0 # homeassistant.components.ps4 pyps4-homeassistant==0.8.7 From 911d2893339c59fc817912a3fc171e8d219f0bfc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 16:05:02 +0200 Subject: [PATCH 129/296] Remove deprecated linksys_ap integration (ADR-0004) (#26847) --- .coveragerc | 1 - .../components/linksys_ap/__init__.py | 1 - .../components/linksys_ap/device_tracker.py | 107 ------------------ .../components/linksys_ap/manifest.json | 10 -- requirements_all.txt | 1 - 5 files changed, 120 deletions(-) delete mode 100644 homeassistant/components/linksys_ap/__init__.py delete mode 100644 homeassistant/components/linksys_ap/device_tracker.py delete mode 100644 homeassistant/components/linksys_ap/manifest.json diff --git a/.coveragerc b/.coveragerc index 9fe3e10c8bc27a..f8c632f7053572 100644 --- a/.coveragerc +++ b/.coveragerc @@ -348,7 +348,6 @@ omit = homeassistant/components/lifx_legacy/light.py homeassistant/components/lightwave/* homeassistant/components/limitlessled/light.py - homeassistant/components/linksys_ap/device_tracker.py homeassistant/components/linksys_smart/device_tracker.py homeassistant/components/linky/__init__.py homeassistant/components/linky/sensor.py diff --git a/homeassistant/components/linksys_ap/__init__.py b/homeassistant/components/linksys_ap/__init__.py deleted file mode 100644 index 5898aa36e98524..00000000000000 --- a/homeassistant/components/linksys_ap/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The linksys_ap component.""" diff --git a/homeassistant/components/linksys_ap/device_tracker.py b/homeassistant/components/linksys_ap/device_tracker.py deleted file mode 100644 index d40de718f902ca..00000000000000 --- a/homeassistant/components/linksys_ap/device_tracker.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Support for Linksys Access Points.""" -import base64 -import logging - -import requests -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA, - DeviceScanner, -) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL - -INTERFACES = 2 -DEFAULT_TIMEOUT = 10 - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - } -) - - -def get_scanner(hass, config): - """Validate the configuration and return a Linksys AP scanner.""" - _LOGGER.warning( - "The linksys_ap integration is deprecated and will be removed " - "in Home Assistant 0.100.0. For more information see ADR-0004:" - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - try: - return LinksysAPDeviceScanner(config[DOMAIN]) - except ConnectionError: - return None - - -class LinksysAPDeviceScanner(DeviceScanner): - """This class queries a Linksys Access Point.""" - - def __init__(self, config): - """Initialize the scanner.""" - self.host = config[CONF_HOST] - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] - self.verify_ssl = config[CONF_VERIFY_SSL] - self.last_results = [] - - # Check if the access point is accessible - response = self._make_request() - if not response.status_code == 200: - raise ConnectionError("Cannot connect to Linksys Access Point") - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - - return self.last_results - - def get_device_name(self, device): - """ - Return the name (if known) of the device. - - Linksys does not provide an API to get a name for a device, - so we just return None - """ - return None - - def _update_info(self): - """Check for connected devices.""" - from bs4 import BeautifulSoup as BS - - _LOGGER.info("Checking Linksys AP") - - self.last_results = [] - for interface in range(INTERFACES): - request = self._make_request(interface) - self.last_results.extend( - [ - x.find_all("td")[1].text - for x in BS(request.content, "html.parser").find_all( - class_="section-row" - ) - ] - ) - - return True - - def _make_request(self, unit=0): - """Create a request to get the data.""" - # No, the '&&' is not a typo - this is expected by the web interface. - login = base64.b64encode(bytes(self.username, "utf8")).decode("ascii") - pwd = base64.b64encode(bytes(self.password, "utf8")).decode("ascii") - url = "https://{}/StatusClients.htm&&unit={}&vap=0".format(self.host, unit) - return requests.get( - url, - timeout=DEFAULT_TIMEOUT, - verify=self.verify_ssl, - cookies={"LoginName": login, "LoginPWD": pwd}, - ) diff --git a/homeassistant/components/linksys_ap/manifest.json b/homeassistant/components/linksys_ap/manifest.json deleted file mode 100644 index 31fafe17edd67f..00000000000000 --- a/homeassistant/components/linksys_ap/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "linksys_ap", - "name": "Linksys ap", - "documentation": "https://www.home-assistant.io/components/linksys_ap", - "requirements": [ - "beautifulsoup4==4.8.0" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/requirements_all.txt b/requirements_all.txt index 07dd2c3965193c..c8f95e29703fdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -260,7 +260,6 @@ batinfo==0.4.2 # homeassistant.components.eddystone_temperature # beacontools[scan]==1.2.3 -# homeassistant.components.linksys_ap # homeassistant.components.scrape beautifulsoup4==4.8.0 From 9c0fbfd101210e27ad162bce355c32ada0b33ab1 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 23 Sep 2019 13:35:27 -0400 Subject: [PATCH 130/296] Bump up ZHA dependencies (#26746) * Update ZHA to track zigpy changes. * Update ZHA dependencies. * Update tests. * Make coverage happy again. --- .coveragerc | 1 + homeassistant/components/zha/core/gateway.py | 2 +- homeassistant/components/zha/core/patches.py | 18 +++--------------- homeassistant/components/zha/manifest.json | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 4 ++-- tests/components/zha/test_binary_sensor.py | 4 ++-- tests/components/zha/test_device_tracker.py | 4 ++-- tests/components/zha/test_fan.py | 4 ++-- tests/components/zha/test_light.py | 8 ++++---- tests/components/zha/test_lock.py | 4 ++-- tests/components/zha/test_sensor.py | 2 +- tests/components/zha/test_switch.py | 4 ++-- 13 files changed, 30 insertions(+), 41 deletions(-) diff --git a/.coveragerc b/.coveragerc index f8c632f7053572..88fdbd45f9a017 100644 --- a/.coveragerc +++ b/.coveragerc @@ -762,6 +762,7 @@ omit = homeassistant/components/zha/core/device.py homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/helpers.py + homeassistant/components/zha/core/patches.py homeassistant/components/zha/core/registries.py homeassistant/components/zha/device_entity.py homeassistant/components/zha/entity.py diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index be09312f6931cb..d2f842956dabef 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -310,7 +310,7 @@ def _async_get_or_create_device(self, zigpy_device): @callback def async_device_became_available( - self, sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args + self, sender, profile, cluster, src_ep, dst_ep, message ): """Handle tasks when a device becomes available.""" self.async_update_device(sender) diff --git a/homeassistant/components/zha/core/patches.py b/homeassistant/components/zha/core/patches.py index 75ef8cce19d18d..d64839026025ea 100644 --- a/homeassistant/components/zha/core/patches.py +++ b/homeassistant/components/zha/core/patches.py @@ -9,9 +9,7 @@ def apply_application_controller_patch(zha_gateway): """Apply patches to ZHA objects.""" # Patch handle_message until zigpy can provide an event here - def handle_message( - sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args - ): + def handle_message(sender, profile, cluster, src_ep, dst_ep, message): """Handle message from a device.""" if ( not sender.initializing @@ -19,18 +17,8 @@ def handle_message( and not zha_gateway.devices[sender.ieee].available ): zha_gateway.async_device_became_available( - sender, - is_reply, - profile, - cluster, - src_ep, - dst_ep, - tsn, - command_id, - args, + sender, profile, cluster, src_ep, dst_ep, message ) - return sender.handle_message( - is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args - ) + return sender.handle_message(profile, cluster, src_ep, dst_ep, message) zha_gateway.application_controller.handle_message = handle_message diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e78661a04e534d..7744b9f223339f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ - "bellows-homeassistant==0.9.1", + "bellows-homeassistant==0.10.0", "zha-quirks==0.0.23", - "zigpy-deconz==0.3.0", - "zigpy-homeassistant==0.8.0", - "zigpy-xbee-homeassistant==0.4.0", + "zigpy-deconz==0.4.0", + "zigpy-homeassistant==0.9.0", + "zigpy-xbee-homeassistant==0.5.0", "zigpy-zigate==0.3.1" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index c8f95e29703fdb..c9b28ae1a14062 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ beautifulsoup4==4.8.0 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.9.1 +bellows-homeassistant==0.10.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.6.0 @@ -2020,13 +2020,13 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.3.0 +zigpy-deconz==0.4.0 # homeassistant.components.zha -zigpy-homeassistant==0.8.0 +zigpy-homeassistant==0.9.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.4.0 +zigpy-xbee-homeassistant==0.5.0 # homeassistant.components.zha zigpy-zigate==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ece1a030aae6a..333c644ebf6be0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.9.1 +bellows-homeassistant==0.10.0 # homeassistant.components.caldav caldav==0.6.1 @@ -434,4 +434,4 @@ wakeonlan==1.1.6 zeroconf==0.23.0 # homeassistant.components.zha -zigpy-homeassistant==0.8.0 +zigpy-homeassistant==0.9.0 diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 7ba7d94d251855..47f81787acd9a0 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -74,13 +74,13 @@ async def async_test_binary_sensor_on_off(hass, cluster, entity_id): """Test getting on and off messages for binary sensors.""" # binary sensor on attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # binary sensor off attr.value.value = 0 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index b4c313041f3ea5..6a7638d9f868c0 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -67,10 +67,10 @@ async def test_device_tracker(hass, config_entry, zha_gateway): # turn state flip attr = make_attribute(0x0020, 23) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) attr = make_attribute(0x0021, 200) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) zigpy_device.last_seen = time.time() + 10 next_update = dt_util.utcnow() + timedelta(seconds=30) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index a8c55890acf018..3fe5e7937c86e2 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -44,13 +44,13 @@ async def test_fan(hass, config_entry, zha_gateway): # turn on at fan attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at fan attr.value.value = 0 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 3101abc52649fa..08c6cfe18cf211 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -123,13 +123,13 @@ async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at light attr.value.value = 0 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -138,7 +138,7 @@ async def async_test_on_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -243,7 +243,7 @@ async def async_test_level_on_off_from_hass( async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): """Test dimmer functionality from the light.""" attr = make_attribute(0, level) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == expected_state # hass uses None for brightness of 0 in state attributes diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 49a60a0f760028..7381b557310b7b 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -43,13 +43,13 @@ async def test_lock(hass, config_entry, zha_gateway): # set state to locked attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_LOCKED # set state to unlocked attr.value.value = 2 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNLOCKED diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index dc3ea35229f2b4..faa44f349272a0 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -177,7 +177,7 @@ async def send_attribute_report(hass, cluster, attrid, value): device is paired to the zigbee network. """ attr = make_attribute(attrid, value) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 07d5bd8ca7c7c5..ac6bc73b809db6 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -44,13 +44,13 @@ async def test_switch(hass, config_entry, zha_gateway): # turn on at switch attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at switch attr.value.value = 0 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF From 5e6840d8f47730a5d0538a1201179f44ec8b0585 Mon Sep 17 00:00:00 2001 From: Balazs Sandor Date: Mon, 23 Sep 2019 19:41:35 +0200 Subject: [PATCH 131/296] fix onvif/camera setting up error (#26825) --- homeassistant/components/onvif/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 4fdd513f840f28..0c116568780a50 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -156,7 +156,7 @@ async def async_initialize(self): Initializes the camera by obtaining the input uri and connecting to the camera. Also retrieves the ONVIF profiles. """ - from aiohttp.client_exceptions import ClientConnectorError + from aiohttp.client_exceptions import ClientConnectionError from homeassistant.exceptions import PlatformNotReady from zeep.exceptions import Fault @@ -167,7 +167,7 @@ async def async_initialize(self): await self.async_check_date_and_time() await self.async_obtain_input_uri() self.setup_ptz() - except ClientConnectorError as err: + except ClientConnectionError as err: _LOGGER.warning( "Couldn't connect to camera '%s', but will " "retry later. Error: %s", self._name, From a7579924098b1e79bf070380fd54da4abe30e722 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 23 Sep 2019 19:42:09 +0200 Subject: [PATCH 132/296] Bump homematicip_cloud to 0.10.11 (#26852) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 2a041ce6689e77..b83358822b9f3f 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/homematicip_cloud", "requirements": [ - "homematicip==0.10.10" + "homematicip==0.10.11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c9b28ae1a14062..62218b14780b03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -646,7 +646,7 @@ homeassistant-pyozw==0.1.4 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.10 +homematicip==0.10.11 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 333c644ebf6be0..b824054a93afb9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -187,7 +187,7 @@ home-assistant-frontend==20190919.0 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.10 +homematicip==0.10.11 # homeassistant.components.google # homeassistant.components.remember_the_milk From 0c78f9e20c645bbf961d5438b5a5d1abe03c77ec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 Sep 2019 13:18:28 -0700 Subject: [PATCH 133/296] Updated frontend to 20190919.1 --- homeassistant/components/frontend/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 896867fcb172eb..605c7f9d7e3464 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", - "requirements": ["home-assistant-frontend==20190919.0"], + "home-assistant-frontend==20190919.1" "dependencies": [ "api", "auth", From 9401d9e28677f57d729b02cb6cd22b545caa6bf7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 Sep 2019 14:20:27 -0700 Subject: [PATCH 134/296] Fix frontend --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 605c7f9d7e3464..e50989a15df3d4 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", - "home-assistant-frontend==20190919.1" + "requirements": ["home-assistant-frontend==20190919.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 842cf4840c832a..47325a6c930e61 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190919.0 +home-assistant-frontend==20190919.1 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 62218b14780b03..04b6eaeb3e9db3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -637,7 +637,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190919.0 +home-assistant-frontend==20190919.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b824054a93afb9..b962ca1280b5d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -181,7 +181,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190919.0 +home-assistant-frontend==20190919.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 5d49d9c951943ddf13bc9cb56cee21700054b7fd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 24 Sep 2019 00:32:13 +0000 Subject: [PATCH 135/296] [ci skip] Translation update --- .../ambiclimate/.translations/no.json | 2 +- .../binary_sensor/.translations/ca.json | 65 +++++++++++++++++++ .../binary_sensor/.translations/ro.json | 45 +++++++++++++ .../binary_sensor/.translations/ru.json | 15 +++++ .../components/izone/.translations/da.json | 15 +++++ .../logi_circle/.translations/no.json | 2 +- .../components/plex/.translations/ca.json | 12 ++++ .../components/plex/.translations/da.json | 45 +++++++++++++ .../components/plex/.translations/no.json | 14 +++- .../components/plex/.translations/ro.json | 24 +++++++ .../components/plex/.translations/ru.json | 12 ++++ .../smartthings/.translations/no.json | 2 +- 12 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/binary_sensor/.translations/ca.json create mode 100644 homeassistant/components/binary_sensor/.translations/ro.json create mode 100644 homeassistant/components/binary_sensor/.translations/ru.json create mode 100644 homeassistant/components/izone/.translations/da.json create mode 100644 homeassistant/components/plex/.translations/da.json create mode 100644 homeassistant/components/plex/.translations/ro.json diff --git a/homeassistant/components/ambiclimate/.translations/no.json b/homeassistant/components/ambiclimate/.translations/no.json index 7bb124ae5433e1..e84de4ffc2261f 100644 --- a/homeassistant/components/ambiclimate/.translations/no.json +++ b/homeassistant/components/ambiclimate/.translations/no.json @@ -9,7 +9,7 @@ "default": "Vellykket autentisering med Ambiclimate" }, "error": { - "follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker p\u00e5 Send", + "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker p\u00e5 Send", "no_token": "Ikke autentisert med Ambiclimate" }, "step": { diff --git a/homeassistant/components/binary_sensor/.translations/ca.json b/homeassistant/components/binary_sensor/.translations/ca.json new file mode 100644 index 00000000000000..434c236418b37d --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/ca.json @@ -0,0 +1,65 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "Bateria de {entity_name} baixa", + "is_connected": "{entity_name} est\u00e0 connectat", + "is_gas": "{entity_name} est\u00e0 detectant gas", + "is_light": "{entity_name} est\u00e0 detectant llum", + "is_locked": "{entity_name} est\u00e0 bloquejat", + "is_motion": "{entity_name} est\u00e0 detectant moviment", + "is_no_gas": "{entity_name} no detecta gas", + "is_no_light": "{entity_name} no detecta llum", + "is_no_motion": "{entity_name} no detecta moviment", + "is_no_smoke": "{entity_name} no detecta fum", + "is_no_sound": "{entity_name} no detecta so", + "is_no_vibration": "{entity_name} no detecta vibraci\u00f3", + "is_not_bat_low": "Bateria de {entity_name} normal", + "is_not_connected": "{entity_name} est\u00e0 desconnectat", + "is_not_locked": "{entity_name} est\u00e0 desbloquejat", + "is_not_occupied": "{entity_name} no est\u00e0 ocupat", + "is_not_powered": "{entity_name} no est\u00e0 alimentat", + "is_not_present": "{entity_name} no est\u00e0 present", + "is_occupied": "{entity_name} est\u00e0 ocupat", + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s", + "is_open": "{entity_name} est\u00e0 obert", + "is_powered": "{entity_name} est\u00e0 alimentat", + "is_present": "{entity_name} est\u00e0 present", + "is_smoke": "{entity_name} est\u00e0 detectant fum", + "is_sound": "{entity_name} est\u00e0 detectant so", + "is_unsafe": "{entity_name} \u00e9s insegur", + "is_vibration": "{entity_name} est\u00e0 detectant vibraci\u00f3" + }, + "trigger_type": { + "bat_low": "Bateria de {entity_name} baixa", + "closed": "{entity_name} est\u00e0 tancat", + "connected": "{entity_name} est\u00e0 connectat", + "gas": "{entity_name} ha comen\u00e7at a detectar gas", + "light": "{entity_name} ha comen\u00e7at a detectar llum", + "locked": "{entity_name} est\u00e0 bloquejat", + "motion": "{entity_name} ha comen\u00e7at a detectar moviment", + "moving": "{entity_name} ha comen\u00e7at a moure's", + "no_gas": "{entity_name} ha deixat de detectar gas", + "no_light": "{entity_name} ha deixat de detectar llum", + "no_motion": "{entity_name} ha deixat de detectar moviment", + "no_problem": "{entity_name} ha deixat de detectar un problema", + "no_smoke": "{entity_name} ha deixat de detectar fum", + "no_sound": "{entity_name} ha deixat de detectar so", + "no_vibration": "{entity_name} ha deixat de detectar vibraci\u00f3", + "not_bat_low": "Bateria de {entity_name} normal", + "not_connected": "{entity_name} est\u00e0 desconnectat", + "not_locked": "{entity_name} est\u00e0 desbloquejat", + "not_moving": "{entity_name} ha parat de moure's", + "not_powered": "{entity_name} no est\u00e0 alimentat", + "not_present": "{entity_name} no est\u00e0 present", + "powered": "{entity_name} alimentat", + "present": "{entity_name} present", + "problem": "{entity_name} ha comen\u00e7at a detectar un problema", + "smoke": "{entity_name} ha comen\u00e7at a detectar fum", + "sound": "{entity_name} ha comen\u00e7at a detectar so", + "turned_off": "{entity_name} apagat", + "turned_on": "{entity_name} enc\u00e8s", + "vibration": "{entity_name} ha comen\u00e7at a detectar vibraci\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ro.json b/homeassistant/components/binary_sensor/.translations/ro.json new file mode 100644 index 00000000000000..438822a97f5bf5 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/ro.json @@ -0,0 +1,45 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} oprit", + "is_on": "{entity_name} pornit" + }, + "trigger_type": { + "gas": "{entity_name} a \u00eenceput s\u0103 detecteze gaz", + "hot": "{entity_name} a devenit fierbinte", + "locked": "{entity_name} blocat", + "motion": "{entity_name} a \u00eenceput s\u0103 detecteze mi\u0219care", + "moving": "{entity_name} a \u00eenceput s\u0103 se mi\u0219te", + "no_light": "{entity_name} a oprit detectarea luminii", + "no_motion": "{entity_name} a oprit detectarea mi\u0219c\u0103rii", + "no_problem": "{entity_name} a oprit detectarea problemei", + "no_smoke": "{entity_name} a oprit detectarea fumului", + "no_sound": "{entity_name} a oprit detectarea de sunet", + "no_vibration": "{entity_name} a oprit detectarea vibra\u021biilor", + "not_bat_low": "{entity_name} baterie normal\u0103", + "not_cold": "{entity_name} nu mai este rece", + "not_connected": "{entity_name} deconectat", + "not_hot": "{entity_name} nu mai este fierbinte", + "not_locked": "{entity_name} deblocat", + "not_moist": "{entity_name} a devenit uscat", + "not_moving": "{entity_name} a \u00eencetat mi\u0219carea", + "not_occupied": "{entity_name} a devenit neocupat", + "not_plugged_in": "{entity_name} deconectat", + "not_powered": "{entity_name} nu este alimentat", + "not_present": "{entity_name} nu este prezent", + "not_unsafe": "{entity_name} a devenit sigur", + "occupied": "{entity_name} a devenit ocupat", + "opened": "{entity_name} deschis", + "plugged_in": "{entity_name} conectat", + "powered": "{entity_name} alimentat", + "present": "{entity_name} prezent", + "problem": "{entity_name} a \u00eenceput detectarea unei probleme", + "smoke": "{entity_name} a \u00eenceput s\u0103 detecteze fum", + "sound": "{entity_name} a \u00eenceput s\u0103 detecteze sunetul", + "turned_off": "{entity_name} oprit", + "turned_on": "{entity_name} pornit", + "unsafe": "{entity_name} a devenit nesigur", + "vibration": "{entity_name} a \u00eenceput s\u0103 detecteze vibra\u021biile" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json new file mode 100644 index 00000000000000..7d73cb8d4aa489 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name}: \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "is_cold": "{entity_name}: \u0445\u043e\u043b\u043e\u0434\u043d\u043e", + "is_connected": "{entity_name}: \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "is_gas": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0433\u0430\u0437", + "is_hot": "{entity_name}: \u0433\u043e\u0440\u044f\u0447\u043e", + "is_light": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0441\u0432\u0435\u0442", + "is_locked": "{entity_name}: \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e", + "is_moist": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0432\u043b\u0430\u0433\u0430", + "is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/da.json b/homeassistant/components/izone/.translations/da.json new file mode 100644 index 00000000000000..9dc3d88322c25e --- /dev/null +++ b/homeassistant/components/izone/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Der blev ikke fundet nogen iZone-enheder p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af iZone" + }, + "step": { + "confirm": { + "description": "Vil du konfigurere iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/no.json b/homeassistant/components/logi_circle/.translations/no.json index 03c128f636ca15..c68f49509baae8 100644 --- a/homeassistant/components/logi_circle/.translations/no.json +++ b/homeassistant/components/logi_circle/.translations/no.json @@ -12,7 +12,7 @@ "error": { "auth_error": "API-autorisasjonen mislyktes.", "auth_timeout": "Autorisasjon ble tidsavbrutt da du ba om token.", - "follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker send." + "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker send." }, "step": { "auth": { diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index eb4f6459f4dcbe..4c24dddbe87917 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "Ha fallat l'autoritzaci\u00f3", "no_servers": "No hi ha servidors enlla\u00e7ats amb el compte", + "no_token": "Proporciona un testimoni d'autenticaci\u00f3 o selecciona configuraci\u00f3 manual", "not_found": "No s'ha trobat el servidor Plex" }, "step": { + "manual_setup": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port", + "ssl": "Utilitza SSL", + "token": "Testimoni d'autenticaci\u00f3 (si \u00e9s necessari)", + "verify_ssl": "Verifica el certificat SSL" + }, + "title": "Servidor Plex" + }, "select_server": { "data": { "server": "Servidor" @@ -22,6 +33,7 @@ }, "user": { "data": { + "manual_setup": "Configuraci\u00f3 manual", "token": "Testimoni d'autenticaci\u00f3 Plex" }, "description": "Introdueix un testimoni d'autenticaci\u00f3 Plex per configurar-ho autom\u00e0ticament.", diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json new file mode 100644 index 00000000000000..ea680a638eb471 --- /dev/null +++ b/homeassistant/components/plex/.translations/da.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "all_configured": "Alle linkede servere er allerede konfigureret", + "already_configured": "Denne Plex-server er allerede konfigureret", + "already_in_progress": "Plex konfigureres", + "invalid_import": "Importeret konfiguration er ugyldig", + "unknown": "Mislykkedes af ukendt \u00e5rsag" + }, + "error": { + "faulty_credentials": "Godkendelse mislykkedes", + "no_servers": "Ingen servere knyttet til konto", + "no_token": "Angiv et token eller v\u00e6lg manuel ops\u00e6tning", + "not_found": "Plex-server ikke fundet" + }, + "step": { + "manual_setup": { + "data": { + "host": "V\u00e6rt", + "port": "Port", + "ssl": "Brug SSL", + "token": "Token (hvis n\u00f8dvendigt)", + "verify_ssl": "Bekr\u00e6ft SSL-certifikat" + }, + "title": "Plex-server" + }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Flere servere til r\u00e5dighed, v\u00e6lg en:", + "title": "V\u00e6lg Plex-server" + }, + "user": { + "data": { + "manual_setup": "Manuel ops\u00e6tning", + "token": "Plex token" + }, + "description": "Indtast et Plex-token til automatisk ops\u00e6tning eller konfigurerer en server manuelt.", + "title": "Tilslut Plex-server" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index 8ac90efe3d1474..b58cdfe728e043 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "Autorisasjonen mislyktes", "no_servers": "Ingen servere koblet til kontoen", + "no_token": "Angi et token eller velg manuelt oppsett", "not_found": "Plex-server ikke funnet" }, "step": { + "manual_setup": { + "data": { + "host": "Vert", + "port": "Port", + "ssl": "Bruk SSL", + "token": "Token (hvis n\u00f8dvendig)", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "title": "Plex server" + }, "select_server": { "data": { "server": "Server" @@ -22,9 +33,10 @@ }, "user": { "data": { + "manual_setup": "Manuelt oppsett", "token": "Plex token" }, - "description": "Legg inn et Plex-token for automatisk oppsett.", + "description": "Angi et Plex-token for automatisk oppsett eller Konfigurer en servern manuelt.", "title": "Koble til Plex-server" } }, diff --git a/homeassistant/components/plex/.translations/ro.json b/homeassistant/components/plex/.translations/ro.json new file mode 100644 index 00000000000000..537bd5e3fac49f --- /dev/null +++ b/homeassistant/components/plex/.translations/ro.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "no_token": "Furniza\u021bi un token sau selecta\u021bi configurarea manual\u0103" + }, + "step": { + "manual_setup": { + "data": { + "host": "Gazd\u0103", + "port": "Port", + "ssl": "Folosi\u021bi SSL", + "token": "Token-ul (dac\u0103 este necesar)", + "verify_ssl": "Verifica\u021bi certificatul SSL" + }, + "title": "Server Plex" + }, + "user": { + "data": { + "manual_setup": "Configurare manual\u0103" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index 46cd613df4ac70..b906d0d8dc6c19 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e", + "no_token": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443", "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d" }, "step": { + "manual_setup": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL", + "token": "\u0422\u043e\u043a\u0435\u043d (\u0435\u0441\u043b\u0438 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f)", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "title": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex" + }, "select_server": { "data": { "server": "\u0421\u0435\u0440\u0432\u0435\u0440" @@ -22,6 +33,7 @@ }, "user": { "data": { + "manual_setup": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "token": "\u0422\u043e\u043a\u0435\u043d" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d Plex \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", diff --git a/homeassistant/components/smartthings/.translations/no.json b/homeassistant/components/smartthings/.translations/no.json index fe93407b429445..b539e315ea3beb 100644 --- a/homeassistant/components/smartthings/.translations/no.json +++ b/homeassistant/components/smartthings/.translations/no.json @@ -5,7 +5,7 @@ "app_setup_error": "Kan ikke konfigurere SmartApp. Vennligst pr\u00f8v p\u00e5 nytt.", "base_url_not_https": "`base_url` for `http` komponenten m\u00e5 konfigureres og starte med `https://`.", "token_already_setup": "Token har allerede blitt satt opp.", - "token_forbidden": "Tollet har ikke de n\u00f8dvendige OAuth m\u00e5lene.", + "token_forbidden": "Tokenet har ikke de n\u00f8dvendige OAuth-omfangene.", "token_invalid_format": "Token m\u00e5 v\u00e6re i UID/GUID format", "token_unauthorized": "Tollet er ugyldig eller ikke lenger autorisert.", "webhook_error": "SmartThings kunne ikke validere endepunktet konfigurert i `base_url`. Vennligst se komponent krav." From 44082869b42ba1c9b5a3849136d68d5fddc27984 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 24 Sep 2019 02:55:11 +0200 Subject: [PATCH 136/296] Group Linky sensors to Linky meter device (#26738) * Group Linky sensors to Llnky meter device * Fix Linky meter identifiers --- homeassistant/components/linky/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index 5ff04c5ee70a38..489e66c2b12482 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -145,8 +145,8 @@ def device_state_attributes(self): def device_info(self): """Return device information.""" return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, + "identifiers": {(DOMAIN, self._username)}, + "name": "Linky meter", "manufacturer": "Enedis", } From 6fe5582c6a4318a202d27919b4aa9e18e6b27528 Mon Sep 17 00:00:00 2001 From: Tim McCormick Date: Tue, 24 Sep 2019 02:45:14 +0100 Subject: [PATCH 137/296] Add unit to 'charging_level_hv' bwm_connected_drive sensor (#26861) * Update sensor.py Add unit to charging_level_hv, which allows correct graphing in UI * Update sensor.py Add space after # --- homeassistant/components/bmw_connected_drive/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 96d541b1955337..28a4e853f2cb6f 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -24,6 +24,8 @@ "remaining_fuel": ["mdi:gas-station", VOLUME_LITERS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], + # No icon as this is dealt with directly as a special case in icon() + "charging_level_hv": [None, "%"], } ATTR_TO_HA_IMPERIAL = { @@ -35,6 +37,8 @@ "remaining_fuel": ["mdi:gas-station", VOLUME_GALLONS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], + # No icon as this is dealt with directly as a special case in icon() + "charging_level_hv": [None, "%"], } From 53e6b8ade6846475adfc9f752826df9e36925e09 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 Sep 2019 23:23:53 -0700 Subject: [PATCH 138/296] Add reproduce state template (#26866) * Add reproduce state template * Handle invalid state --- script/scaffold/__main__.py | 10 ++- script/scaffold/docs.py | 11 +++ .../integration/reproduce_state.py | 78 +++++++++++++++++++ .../tests/test_reproduce_state.py | 56 +++++++++++++ setup.cfg | 4 +- 5 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 script/scaffold/templates/reproduce_state/integration/reproduce_state.py create mode 100644 script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 93bcc5aba4172c..22cdee8f69e0e5 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -4,7 +4,7 @@ import subprocess import sys -from . import gather_info, generate, error +from . import gather_info, generate, error, docs from .const import COMPONENT_DIR @@ -65,9 +65,11 @@ def main(): print() print("Running tests") - print(f"$ pytest tests/components/{info.domain}") + print(f"$ pytest -v tests/components/{info.domain}") if ( - subprocess.run(f"pytest tests/components/{info.domain}", shell=True).returncode + subprocess.run( + f"pytest -v tests/components/{info.domain}", shell=True + ).returncode != 0 ): return 1 @@ -75,6 +77,8 @@ def main(): print(f"Done!") + docs.print_relevant_docs(args.template, info) + return 0 diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 54a182be31bd63..801b8ebb5fd8b0 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -18,5 +18,16 @@ def print_relevant_docs(template: str, info: Info) -> None: print( f""" The config flow has been added to the {info.domain} integration. Next step is to fill in the blanks for the code marked with TODO. +""" + ) + + elif template == "reproduce_state": + print( + f""" +Reproduce state code has been added to the {info.domain} integration: + - {info.integration_dir / "reproduce_state.py"} + - {info.tests_dir / "test_reproduce_state.py"} + +Please update the relevant items marked as TODO before submitting a pull request. """ ) diff --git a/script/scaffold/templates/reproduce_state/integration/reproduce_state.py b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py new file mode 100644 index 00000000000000..3449009818b6ab --- /dev/null +++ b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py @@ -0,0 +1,78 @@ +"""Reproduce an NEW_NAME state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +# TODO add valid states here +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if ( + cur_state.state == state.state + and + # TODO this is an example attribute + cur_state.attributes.get("color") == state.attributes.get("color") + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + # TODO determine the services to call to achieve desired state + if state.state == STATE_ON: + service = SERVICE_TURN_ON + if "color" in state.attributes: + service_data["color"] = state.attributes["color"] + + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce NEW_NAME states.""" + # TODO pick one and remove other one + + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) + + # Alternative: Reproduce states in sequence + # for state in states: + # await _async_reproduce_state(hass, state, context) diff --git a/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py new file mode 100644 index 00000000000000..ff15625ad7c4fb --- /dev/null +++ b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py @@ -0,0 +1,56 @@ +"""Test reproduce state for NEW_NAME.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing NEW_NAME states.""" + hass.states.async_set("NEW_DOMAIN.entity_off", "off", {}) + hass.states.async_set("NEW_DOMAIN.entity_on", "on", {"color": "red"}) + + turn_on_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_on") + turn_off_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("NEW_DOMAIN.entity_off", "off"), + State("NEW_DOMAIN.entity_on", "on", {"color": "red"}), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("NEW_DOMAIN.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("NEW_DOMAIN.entity_on", "off"), + State("NEW_DOMAIN.entity_off", "on", {"color": "red"}), + # Should not raise + State("NEW_DOMAIN.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "NEW_DOMAIN" + assert turn_on_calls[0].data == { + "entity_id": "NEW_DOMAIN.entity_off", + "color": "red", + } + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "NEW_DOMAIN" + assert turn_off_calls[0].data == {"entity_id": "NEW_DOMAIN.entity_on"} diff --git a/setup.cfg b/setup.cfg index 49f738cf969eb7..4c9c892b93fbb4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,11 +27,13 @@ max-line-length = 88 # W503: Line break occurred before a binary operator # E203: Whitespace before ':' # D202 No blank lines allowed after function docstring +# W504 line break after binary operator ignore = E501, W503, E203, - D202 + D202, + W504 [isort] # https://github.com/timothycrosley/isort From 1d60cccc2183cec1b36bb81bdea576006c1c7dba Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 24 Sep 2019 11:09:16 +0100 Subject: [PATCH 139/296] Put draw_box in image_processing (#26712) * Put draw_box in image_processing * Update draw_box * Update __init__.py * run isort * Fix lints * Update __init__.py * Update requirements_all.txt * Adds type hints * Update gen_requirements_all.py * Update requirements_test_all.txt * rerun script/gen_requirements_all.py * Change Pillow to pillow * Update manifest.json * Run script/gen_requirements_all.py --- .../components/doods/image_processing.py | 19 +-------- .../components/image_processing/__init__.py | 40 ++++++++++++++++++- .../components/image_processing/manifest.json | 4 +- .../components/tensorflow/image_processing.py | 19 +-------- .../components/tensorflow/manifest.json | 1 - requirements_all.txt | 2 +- requirements_test_all.txt | 5 +++ script/gen_requirements_all.py | 1 + 8 files changed, 51 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 850eae76040f2a..3eec85b3e5304f 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -14,6 +14,7 @@ CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, + draw_box, ) from homeassistant.core import split_entity_id from homeassistant.helpers import template @@ -68,24 +69,6 @@ ) -def draw_box(draw, box, img_width, img_height, text="", color=(255, 255, 0)): - """Draw bounding box on image.""" - ymin, xmin, ymax, xmax = box - (left, right, top, bottom) = ( - xmin * img_width, - xmax * img_width, - ymin * img_height, - ymax * img_height, - ) - draw.line( - [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], - width=5, - fill=color, - ) - if text: - draw.text((left, abs(top - 15)), text, fill=color) - - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Doods client.""" url = config[CONF_URL] diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index b1c167a4175072..e9621fe6bbe719 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -2,7 +2,9 @@ import asyncio from datetime import timedelta import logging +from typing import Tuple +from PIL import ImageDraw import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_ENTITY_ID, CONF_NAME @@ -14,7 +16,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util.async_ import run_callback_threadsafe - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -64,6 +65,43 @@ PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) +def draw_box( + draw: ImageDraw, + box: Tuple[float, float, float, float], + img_width: int, + img_height: int, + text: str = "", + color: Tuple[int, int, int] = (255, 255, 0), +) -> None: + """ + Draw a bounding box on and image. + + The bounding box is defined by the tuple (y_min, x_min, y_max, x_max) + where the coordinates are floats in the range [0.0, 1.0] and + relative to the width and height of the image. + + For example, if an image is 100 x 200 pixels (height x width) and the bounding + box is `(0.1, 0.2, 0.5, 0.9)`, the upper-left and bottom-right coordinates of + the bounding box will be `(40, 10)` to `(180, 50)` (in (x,y) coordinates). + """ + + line_width = 5 + y_min, x_min, y_max, x_max = box + (left, right, top, bottom) = ( + x_min * img_width, + x_max * img_width, + y_min * img_height, + y_max * img_height, + ) + draw.line( + [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], + width=line_width, + fill=color, + ) + if text: + draw.text((left + line_width, abs(top - line_width)), text, fill=color) + + async def async_setup(hass, config): """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index e675d18a00b7d6..f3a7121c0b4ff3 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -2,7 +2,9 @@ "domain": "image_processing", "name": "Image processing", "documentation": "https://www.home-assistant.io/components/image_processing", - "requirements": [], + "requirements": [ + "pillow==6.1.0" + ], "dependencies": [ "camera" ], diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index b977a087eaeb54..65e20f558a787d 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -12,6 +12,7 @@ CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, + draw_box, ) from homeassistant.core import split_entity_id from homeassistant.helpers import template @@ -67,24 +68,6 @@ ) -def draw_box(draw, box, img_width, img_height, text="", color=(255, 255, 0)): - """Draw bounding box on image.""" - ymin, xmin, ymax, xmax = box - (left, right, top, bottom) = ( - xmin * img_width, - xmax * img_width, - ymin * img_height, - ymax * img_height, - ) - draw.line( - [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], - width=5, - fill=color, - ) - if text: - draw.text((left, abs(top - 15)), text, fill=color) - - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the TensorFlow image processing platform.""" model_config = config.get(CONF_MODEL) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 9419cbaaefbede..279ac3b103cba8 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -5,7 +5,6 @@ "requirements": [ "tensorflow==1.13.2", "numpy==1.17.1", - "pillow==6.1.0", "protobuf==3.6.1" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 04b6eaeb3e9db3..037fb1fcd2afdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -950,9 +950,9 @@ piglow==1.2.4 # homeassistant.components.pilight pilight==0.1.1 +# homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -# homeassistant.components.tensorflow pillow==6.1.0 # homeassistant.components.dominos diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b962ca1280b5d8..63ad27a654caa7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -252,6 +252,11 @@ pexpect==4.6.0 # homeassistant.components.pilight pilight==0.1.1 +# homeassistant.components.image_processing +# homeassistant.components.proxy +# homeassistant.components.qrcode +pillow==6.1.0 + # homeassistant.components.plex plexapi==3.0.6 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d74a57d678d494..649c48e1b7d282 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -111,6 +111,7 @@ "paho-mqtt", "pexpect", "pilight", + "pillow", "plexapi", "pmsensor", "prometheus_client", From 930dadb7226c111658d691c13f2a46d9019d9ea7 Mon Sep 17 00:00:00 2001 From: majuss Date: Tue, 24 Sep 2019 13:10:03 +0200 Subject: [PATCH 140/296] Move elv integration to component and bump pypca (#26552) * fixing elv integration * black formatting * linting * rebase * removed logger warning for failed conf * rebase; coverage --- .coveragerc | 2 +- homeassistant/components/elv/__init__.py | 36 ++++++++++++++++++++++ homeassistant/components/elv/manifest.json | 4 +-- homeassistant/components/elv/switch.py | 36 ++++++++-------------- requirements_all.txt | 2 +- 5 files changed, 53 insertions(+), 27 deletions(-) diff --git a/.coveragerc b/.coveragerc index 88fdbd45f9a017..a4d6d0d201e980 100644 --- a/.coveragerc +++ b/.coveragerc @@ -165,7 +165,7 @@ omit = homeassistant/components/eight_sleep/* homeassistant/components/eliqonline/sensor.py homeassistant/components/elkm1/* - homeassistant/components/elv/switch.py + homeassistant/components/elv/* homeassistant/components/emby/media_player.py homeassistant/components/emoncms/sensor.py homeassistant/components/emoncms_history/* diff --git a/homeassistant/components/elv/__init__.py b/homeassistant/components/elv/__init__.py index 13ade253ff659a..b6097737414e91 100644 --- a/homeassistant/components/elv/__init__.py +++ b/homeassistant/components/elv/__init__.py @@ -1 +1,37 @@ """The Elv integration.""" + +import logging + +import voluptuous as vol + +from homeassistant.helpers import discovery +from homeassistant.const import CONF_DEVICE +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "elv" + +DEFAULT_DEVICE = "/dev/ttyUSB0" + +ELV_PLATFORMS = ["switch"] + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string} + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the PCA switch platform.""" + + for platform in ELV_PLATFORMS: + discovery.load_platform( + hass, platform, DOMAIN, {"device": config[DOMAIN][CONF_DEVICE]}, config + ) + + return True diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index 4c9ed56352ecd8..04d384416217a8 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/components/pca", "dependencies": [], "codeowners": ["@majuss"], - "requirements": ["pypca==0.0.4"] - } \ No newline at end of file + "requirements": ["pypca==0.0.5"] + } diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index c6258e244e9c3c..362424c7faca48 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -1,15 +1,11 @@ """Support for PCA 301 smart switch.""" import logging -import voluptuous as vol +import pypca +from serial import SerialException -from homeassistant.components.switch import ( - SwitchDevice, - PLATFORM_SCHEMA, - ATTR_CURRENT_POWER_W, -) -from homeassistant.const import CONF_NAME, CONF_DEVICE, EVENT_HOMEASSISTANT_STOP -import homeassistant.helpers.config_validation as cv +from homeassistant.components.switch import SwitchDevice, ATTR_CURRENT_POWER_W +from homeassistant.const import EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -17,26 +13,20 @@ DEFAULT_NAME = "PCA 301" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_DEVICE): cv.string, - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the PCA switch platform.""" - import pypca - from serial import SerialException - name = config[CONF_NAME] - usb_device = config[CONF_DEVICE] + if discovery_info is None: + return + + serial_device = discovery_info["device"] try: - pca = pypca.PCA(usb_device) + pca = pypca.PCA(serial_device) pca.open() - entities = [SmartPlugSwitch(pca, device, name) for device in pca.get_devices()] + + entities = [SmartPlugSwitch(pca, device) for device in pca.get_devices()] add_entities(entities, True) except SerialException as exc: @@ -51,10 +41,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SmartPlugSwitch(SwitchDevice): """Representation of a PCA Smart Plug switch.""" - def __init__(self, pca, device_id, name): + def __init__(self, pca, device_id): """Initialize the switch.""" self._device_id = device_id - self._name = name + self._name = "PCA 301" self._state = None self._available = True self._emeter_params = {} diff --git a/requirements_all.txt b/requirements_all.txt index 037fb1fcd2afdb..b8d4a158655af8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1375,7 +1375,7 @@ pyowlet==1.0.2 pyowm==2.10.0 # homeassistant.components.elv -pypca==0.0.4 +pypca==0.0.5 # homeassistant.components.lcn pypck==0.6.3 From 161c8aada6fa5b7b6341a583c0cc8a7ea982f563 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Wed, 25 Sep 2019 00:05:19 +1000 Subject: [PATCH 141/296] Add availability_template to Template Sensor platform (#26516) * Added availability_template to Template Sensor platform * Added to test for invalid values in availability_template * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Moved availability_template rendering to common loop * Removed 'Magic' string * Cleaned up const and compare lowercase result to 'true' * Device is unavailbale if value template render fails * Converted test ot Async and fixed states * Comverted back to using boolean for _availability * Fixed state check in test --- homeassistant/components/template/const.py | 3 + homeassistant/components/template/sensor.py | 38 ++++++++---- tests/components/template/test_sensor.py | 67 ++++++++++++++++++++- 3 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/template/const.py diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py new file mode 100644 index 00000000000000..e6cf69341f9de8 --- /dev/null +++ b/homeassistant/components/template/const.py @@ -0,0 +1,3 @@ +"""Constants for the Template Platform Components.""" + +CONF_AVAILABILITY_TEMPLATE = "availability_template" diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index b77528e0c324a7..a876819373652f 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -24,10 +24,12 @@ MATCH_ALL, CONF_DEVICE_CLASS, ) + from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.event import async_track_state_change +from .const import CONF_AVAILABILITY_TEMPLATE CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" @@ -39,6 +41,7 @@ vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( {cv.string: cv.template} ), @@ -62,6 +65,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config[CONF_VALUE_TEMPLATE] icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) @@ -77,6 +81,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_ICON_TEMPLATE: icon_template, CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, CONF_FRIENDLY_NAME_TEMPLATE: friendly_name_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, } for tpl_name, template in chain(templates.items(), attribute_templates.items()): @@ -120,15 +125,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, icon_template, entity_picture_template, + availability_template, entity_ids, device_class, attribute_templates, ) ) - if not sensors: - _LOGGER.error("No sensors added") - return False - async_add_entities(sensors) return True @@ -146,6 +148,7 @@ def __init__( state_template, icon_template, entity_picture_template, + availability_template, entity_ids, device_class, attribute_templates, @@ -162,10 +165,12 @@ def __init__( self._state = None self._icon_template = icon_template self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._icon = None self._entity_picture = None self._entities = entity_ids self._device_class = device_class + self._available = True self._attribute_templates = attribute_templates self._attributes = {} @@ -222,6 +227,11 @@ def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" return self._unit_of_measurement + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + @property def device_state_attributes(self): """Return the state attributes.""" @@ -236,7 +246,9 @@ async def async_update(self): """Update the state from the template.""" try: self._state = self._template.async_render() + self._available = True except TemplateError as ex: + self._available = False if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute" ): @@ -248,12 +260,6 @@ async def async_update(self): self._state = None _LOGGER.error("Could not render template %s: %s", self._name, ex) - templates = { - "_icon": self._icon_template, - "_entity_picture": self._entity_picture_template, - "_name": self._friendly_name_template, - } - attrs = {} for key, value in self._attribute_templates.items(): try: @@ -263,12 +269,22 @@ async def async_update(self): self._attributes = attrs + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + "_name": self._friendly_name_template, + "_available": self._availability_template, + } + for property_name, template in templates.items(): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 9223399bee7af1..b3813da176633c 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -3,6 +3,7 @@ from homeassistant.setup import setup_component, async_setup_component from tests.common import get_test_home_assistant, assert_setup_component +from homeassistant.const import STATE_UNAVAILABLE, STATE_ON, STATE_OFF class TestTemplateSensor: @@ -251,7 +252,7 @@ def test_template_attribute_missing(self): self.hass.block_till_done() state = self.hass.states.get("sensor.test_template_sensor") - assert state.state == "unknown" + assert state.state == STATE_UNAVAILABLE def test_invalid_name_does_not_create(self): """Test invalid name.""" @@ -377,6 +378,44 @@ def test_setup_valid_device_class(self): assert "device_class" not in state.attributes +async def test_available_template_with_entities(hass): + """Test availability tempalates with values from other entities.""" + hass.states.async_set("sensor.availability_sensor", STATE_OFF) + with assert_setup_component(1, "sensor"): + assert await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.test_sensor.state }}", + "availability_template": "{{ is_state('sensor.availability_sensor', 'on') }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set("sensor.availability_sensor", STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get("sensor.test_template_sensor").state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set("sensor.availability_sensor", STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get("sensor.test_template_sensor").state == STATE_UNAVAILABLE + + async def test_invalid_attribute_template(hass, caplog): """Test that errors are logged if rendering template fails.""" hass.states.async_set("sensor.test_sensor", "startup") @@ -405,6 +444,32 @@ async def test_invalid_attribute_template(hass, caplog): assert ("Error rendering attribute test_attribute") in caplog.text +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "my_sensor": { + "value_template": "{{ states.sensor.test_state.state }}", + "availability_template": "{{ x - 12 }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("sensor.my_sensor").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text + + async def test_no_template_match_all(hass, caplog): """Test that we do not allow sensors that match on all.""" hass.states.async_set("sensor.test_sensor", "startup") From 18873d202d58fa41ec57ffa01860b7670d6e224e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 24 Sep 2019 11:54:41 -0400 Subject: [PATCH 142/296] Add device automation support to ZHA (#26821) * beginning ZHA device automations * add cube * load triggers from zigpy devices * cleanup * add face_any for aqara cube * add endpoint id to events * add cluster id to events * cleanup * add device tilt * add test * fix copy paste error * add event trigger test * add test for no triggers for device * add exception test * better exception tests --- .../components/zha/.translations/en.json | 75 ++++- .../components/zha/core/channels/__init__.py | 2 + homeassistant/components/zha/core/device.py | 7 + .../components/zha/device_automation.py | 89 +++++ homeassistant/components/zha/strings.json | 75 ++++- .../components/zha/test_device_automation.py | 308 ++++++++++++++++++ 6 files changed, 524 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/zha/device_automation.py create mode 100644 tests/components/zha/test_device_automation.py diff --git a/homeassistant/components/zha/.translations/en.json b/homeassistant/components/zha/.translations/en.json index f0da251f5eb643..6a819fbc16f8e2 100644 --- a/homeassistant/components/zha/.translations/en.json +++ b/homeassistant/components/zha/.translations/en.json @@ -1,20 +1,63 @@ { - "config": { - "abort": { - "single_instance_allowed": "Only a single configuration of ZHA is allowed." - }, - "error": { - "cannot_connect": "Unable to connect to ZHA device." - }, - "step": { - "user": { - "data": { - "radio_type": "Radio Type", - "usb_path": "USB Device Path" - }, - "title": "ZHA" - } + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of ZHA is allowed." + }, + "error": { + "cannot_connect": "Unable to connect to ZHA device." + }, + "step": { + "user": { + "data": { + "radio_type": "Radio Type", + "usb_path": "USB Device Path" }, "title": "ZHA" + } + }, + "title": "ZHA" + }, + "device_automation": { + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "device_rotated": "Device rotated \"{subtype}\"", + "device_shaken": "Device shaken", + "device_slid": "Device slid \"{subtype}\"", + "device_tilted": "Device tilted", + "device_knocked": "Device knocked \"{subtype}\"", + "device_dropped": "Device dropped", + "device_flipped": "Device flipped \"{subtype}\"" + }, + "trigger_subtype": { + "turn_on": "Turn on", + "turn_off": "Turn off", + "dim_up": "Dim up", + "dim_down": "Dim down", + "left": "Left", + "right": "Right", + "open": "Open", + "close": "Close", + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "face_any": "With any/specified face(s) activated", + "face_1": "With face 1 activated", + "face_2": "With face 2 activated", + "face_3": "With face 3 activated", + "face_4": "With face 4 activated", + "face_5": "With face 5 activated", + "face_6": "With face 6 activated" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index aed12bc65a5495..3d4a03fb0acac0 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -240,6 +240,8 @@ def zha_send_event(self, cluster, command, args): { "unique_id": self._unique_id, "device_ieee": str(self._zha_device.ieee), + "endpoint_id": cluster.endpoint.endpoint_id, + "cluster_id": cluster.cluster_id, "command": command, "args": args, }, diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 1db4aafeeb9a80..82d20ff78c207e 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -187,6 +187,13 @@ def all_channels(self): """Return cluster channels and relay channels for device.""" return self._all_channels + @property + def device_automation_triggers(self): + """Return the device automation triggers for this device.""" + if hasattr(self._zigpy_device, "device_automation_triggers"): + return self._zigpy_device.device_automation_triggers + return None + @property def available_signal(self): """Signal to use to subscribe to device availability changes.""" diff --git a/homeassistant/components/zha/device_automation.py b/homeassistant/components/zha/device_automation.py new file mode 100644 index 00000000000000..6a96ce5aa3e405 --- /dev/null +++ b/homeassistant/components/zha/device_automation.py @@ -0,0 +1,89 @@ +"""Provides device automations for ZHA devices that emit events.""" +import voluptuous as vol + +import homeassistant.components.automation.event as event +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE + +from . import DOMAIN +from .core.const import DATA_ZHA, DATA_ZHA_GATEWAY +from .core.helpers import convert_ieee + +CONF_SUBTYPE = "subtype" +DEVICE = "device" +DEVICE_IEEE = "device_ieee" +ZHA_EVENT = "zha_event" + +TRIGGER_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_PLATFORM): DEVICE, + vol.Required(CONF_TYPE): str, + vol.Required(CONF_SUBTYPE): str, + } + ) +) + + +async def async_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + zha_device = await _async_get_zha_device(hass, config[CONF_DEVICE_ID]) + + if ( + zha_device.device_automation_triggers is None + or trigger not in zha_device.device_automation_triggers + ): + raise InvalidDeviceAutomationConfig + + trigger = zha_device.device_automation_triggers[trigger] + + state_config = { + event.CONF_EVENT_TYPE: ZHA_EVENT, + event.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger}, + } + + return await event.async_trigger(hass, state_config, action, automation_info) + + +async def async_get_triggers(hass, device_id): + """List device triggers. + + Make sure the device supports device automations and + if it does return the trigger list. + """ + zha_device = await _async_get_zha_device(hass, device_id) + + if not zha_device.device_automation_triggers: + return + + triggers = [] + for trigger, subtype in zha_device.device_automation_triggers.keys(): + triggers.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: DEVICE, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + + return triggers + + +async def _async_get_zha_device(hass, device_id): + device_registry = await hass.helpers.device_registry.async_get_registry() + registry_device = device_registry.async_get(device_id) + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee_address = list(list(registry_device.identifiers)[0])[1] + ieee = convert_ieee(ieee_address) + zha_device = zha_gateway.devices[ieee] + if not zha_device: + raise InvalidDeviceAutomationConfig + return zha_device diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index e1ed6a678e3e95..cfc32a020c6dd5 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -1,20 +1,63 @@ { - "config": { + "config": { + "title": "ZHA", + "step": { + "user": { "title": "ZHA", - "step": { - "user": { - "title": "ZHA", - "data": { - "radio_type": "Radio Type", - "usb_path": "USB Device Path" - } - } - }, - "error": { - "cannot_connect": "Unable to connect to ZHA device." - }, - "abort": { - "single_instance_allowed": "Only a single configuration of ZHA is allowed." + "data": { + "radio_type": "Radio Type", + "usb_path": "USB Device Path" } + } + }, + "error": { + "cannot_connect": "Unable to connect to ZHA device." + }, + "abort": { + "single_instance_allowed": "Only a single configuration of ZHA is allowed." } -} \ No newline at end of file + }, + "device_automation": { + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "device_rotated": "Device rotated \"{subtype}\"", + "device_shaken": "Device shaken", + "device_slid": "Device slid \"{subtype}\"", + "device_tilted": "Device tilted", + "device_knocked": "Device knocked \"{subtype}\"", + "device_dropped": "Device dropped", + "device_flipped": "Device flipped \"{subtype}\"" + }, + "trigger_subtype": { + "turn_on": "Turn on", + "turn_off": "Turn off", + "dim_up": "Dim up", + "dim_down": "Dim down", + "left": "Left", + "right": "Right", + "open": "Open", + "close": "Close", + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "face_any": "With any/specified face(s) activated", + "face_1": "with face 1 activated", + "face_2": "with face 2 activated", + "face_3": "with face 3 activated", + "face_4": "with face 4 activated", + "face_5": "with face 5 activated", + "face_6": "with face 6 activated" + } + } +} diff --git a/tests/components/zha/test_device_automation.py b/tests/components/zha/test_device_automation.py new file mode 100644 index 00000000000000..9de04ae8e662e8 --- /dev/null +++ b/tests/components/zha/test_device_automation.py @@ -0,0 +1,308 @@ +"""ZHA device automation tests.""" +from unittest.mock import patch + +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.components.switch import DOMAIN +from homeassistant.components.zha.core.const import CHANNEL_ON_OFF +from homeassistant.helpers.device_registry import async_get_registry +from homeassistant.setup import async_setup_component + +from .common import async_enable_traffic, async_init_zigpy_device + +from tests.common import async_mock_service + +ON = 1 +OFF = 0 +SHAKEN = "device_shaken" +COMMAND = "command" +COMMAND_SHAKE = "shake" +COMMAND_HOLD = "hold" +COMMAND_SINGLE = "single" +COMMAND_DOUBLE = "double" +DOUBLE_PRESS = "remote_button_double_press" +SHORT_PRESS = "remote_button_short_press" +LONG_PRESS = "remote_button_long_press" +LONG_RELEASE = "remote_button_long_release" + + +def _same_lists(list_a, list_b): + if len(list_a) != len(list_b): + return False + + for item in list_a: + if item not in list_b: + return False + return True + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_triggers(hass, config_entry, zha_gateway): + """Test zha device triggers.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + ieee_address = str(zha_device.ieee) + + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + triggers = await async_get_device_automations( + hass, "async_get_triggers", reg_device.id + ) + + expected_triggers = [ + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": SHAKEN, + "subtype": SHAKEN, + }, + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": DOUBLE_PRESS, + "subtype": DOUBLE_PRESS, + }, + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": SHORT_PRESS, + "subtype": SHORT_PRESS, + }, + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": LONG_PRESS, + "subtype": LONG_PRESS, + }, + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": LONG_RELEASE, + "subtype": LONG_RELEASE, + }, + ] + assert _same_lists(triggers, expected_triggers) + + +async def test_no_triggers(hass, config_entry, zha_gateway): + """Test zha device with no triggers.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + ieee_address = str(zha_device.ieee) + + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + triggers = await async_get_device_automations( + hass, "async_get_triggers", reg_device.id + ) + assert triggers == [] + + +async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): + """Test for remote triggers firing.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + ieee_address = str(zha_device.ieee) + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": SHORT_PRESS, + "subtype": SHORT_PRESS, + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + + await hass.async_block_till_done() + + on_off_channel = zha_device.cluster_channels[CHANNEL_ON_OFF] + on_off_channel.zha_send_event(on_off_channel.cluster, COMMAND_SINGLE, []) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["message"] == "service called" + + +async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls): + """Test for exception on event triggers firing.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + ieee_address = str(zha_device.ieee) + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + with patch("logging.Logger.error") as mock: + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + mock.assert_called_with("Error setting up trigger %s", "automation 0") + + +async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls): + """Test for exception on event triggers firing.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + ieee_address = str(zha_device.ieee) + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + with patch("logging.Logger.error") as mock: + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + mock.assert_called_with("Error setting up trigger %s", "automation 0") From b1118cb8ffa4c21a955ae569697a3f81eae94e8a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Sep 2019 22:53:03 +0200 Subject: [PATCH 143/296] Removes unnecessary else/elif blocks (#26884) --- .../eddystone_temperature/sensor.py | 4 +-- homeassistant/components/filesize/sensor.py | 3 +- homeassistant/components/isy994/__init__.py | 6 ++-- homeassistant/components/mvglive/sensor.py | 12 ++++--- .../components/onkyo/media_player.py | 6 ++-- homeassistant/components/recorder/__init__.py | 4 +-- homeassistant/components/todoist/calendar.py | 35 +++++++++++-------- .../components/webostv/media_player.py | 6 ++-- .../components/websocket_api/http.py | 2 +- .../components/zha/core/discovery.py | 4 ++- homeassistant/components/zwave/__init__.py | 7 ++-- homeassistant/helpers/__init__.py | 3 +- homeassistant/scripts/__init__.py | 3 +- 13 files changed, 53 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 5492582ebed27d..67724e9fcf344e 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -60,8 +60,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if instance is None or namespace is None: _LOGGER.error("Skipping %s", dev_name) continue - else: - devices.append(EddystoneTemp(name, namespace, instance)) + + devices.append(EddystoneTemp(name, namespace, instance)) if devices: mon = Monitor(hass, devices, bt_device_id) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index a4b9bc5cd76c8f..af9375aad053d4 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -27,8 +27,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if not hass.config.is_allowed_path(path): _LOGGER.error("Filepath %s is not valid or allowed", path) continue - else: - sensors.append(Filesize(path)) + sensors.append(Filesize(path)) if sensors: add_entities(sensors, True) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 727ec91dc37cd3..324dcb019b3915 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -323,9 +323,9 @@ def _categorize_nodes( # determine if it should be a binary_sensor. if _is_sensor_a_binary_sensor(hass, node): continue - else: - hass.data[ISY994_NODES]["sensor"].append(node) - continue + + hass.data[ISY994_NODES]["sensor"].append(node) + continue # We have a bunch of different methods for determining the device type, # each of which works with different ISY firmware versions or device diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 6da26784d9c5d0..3c753d832e097e 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -189,17 +189,19 @@ def update(self): and _departure["destination"] not in self._destinations ): continue - elif ( + + if ( "" not in self._directions[:1] and _departure["direction"] not in self._directions ): continue - elif ( - "" not in self._lines[:1] and _departure["linename"] not in self._lines - ): + + if "" not in self._lines[:1] and _departure["linename"] not in self._lines: continue - elif _departure["time"] < self._timeoffset: + + if _departure["time"] < self._timeoffset: continue + # now select the relevant data _nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION} for k in ["destination", "linename", "time", "direction", "product"]: diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 023fb32e6e40da..9ec8c56d770a00 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -264,8 +264,7 @@ def update(self): if source in self._source_mapping: self._current_source = self._source_mapping[source] break - else: - self._current_source = "_".join([i for i in current_source_tuples[1]]) + self._current_source = "_".join([i for i in current_source_tuples[1]]) if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] elif ATTR_PRESET in self._attributes: @@ -414,8 +413,7 @@ def update(self): if source in self._source_mapping: self._current_source = self._source_mapping[source] break - else: - self._current_source = "_".join([i for i in current_source_tuples[1]]) + self._current_source = "_".join([i for i in current_source_tuples[1]]) self._muted = bool(mute_raw[1] == "on") if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 9d34cc6fb79f27..b36e0a34fa482a 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -320,10 +320,10 @@ def async_purge(now): purge.purge_old_data(self, event.keep_days, event.repack) self.queue.task_done() continue - elif event.event_type == EVENT_TIME_CHANGED: + if event.event_type == EVENT_TIME_CHANGED: self.queue.task_done() continue - elif event.event_type in self.exclude_t: + if event.event_type in self.exclude_t: self.queue.task_done() continue diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 7d2f51f29af34c..75aec037a25ed5 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -484,39 +484,44 @@ def select_best_task(project_tasks): for proposed_event in project_tasks: if event == proposed_event: continue + if proposed_event[COMPLETED]: # Event is complete! continue + if proposed_event[END] is None: # No end time: if event[END] is None and (proposed_event[PRIORITY] < event[PRIORITY]): # They also have no end time, # but we have a higher priority. event = proposed_event - continue - else: - continue - elif event[END] is None: + continue + + if event[END] is None: # We have an end time, they do not. event = proposed_event continue + if proposed_event[END].date() > event[END].date(): # Event is too late. continue - elif proposed_event[END].date() < event[END].date(): + + if proposed_event[END].date() < event[END].date(): # Event is earlier than current, select it. event = proposed_event continue - else: - if proposed_event[PRIORITY] > event[PRIORITY]: - # Proposed event has a higher priority. - event = proposed_event - continue - elif proposed_event[PRIORITY] == event[PRIORITY] and ( - proposed_event[END] < event[END] - ): - event = proposed_event - continue + + if proposed_event[PRIORITY] > event[PRIORITY]: + # Proposed event has a higher priority. + event = proposed_event + continue + + if proposed_event[PRIORITY] == event[PRIORITY] and ( + proposed_event[END] < event[END] + ): + event = proposed_event + continue + return event async def async_get_events(self, hass, start_date, end_date): diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 1da70bc60ec255..913d193845fb01 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -396,10 +396,12 @@ def play_media(self, media_type, media_id, **kwargs): if media_id == channel["channelNumber"]: perfect_match_channel_id = channel["channelId"] continue - elif media_id.lower() == channel["channelName"].lower(): + + if media_id.lower() == channel["channelName"].lower(): perfect_match_channel_id = channel["channelId"] continue - elif media_id.lower() in channel["channelName"].lower(): + + if media_id.lower() in channel["channelName"].lower(): partial_match_channel_id = channel["channelId"] if perfect_match_channel_id is not None: diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 108fcbc7740d6a..9a1f375fdfda20 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -165,7 +165,7 @@ def handle_hass_stop(event): if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING): break - elif msg.type != WSMsgType.TEXT: + if msg.type != WSMsgType.TEXT: disconnect_warn = "Received non-Text message." break diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 5a5ffb34ab13ea..80642a373da7e4 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -199,11 +199,13 @@ def _async_handle_single_cluster_matches( zha_device.is_mains_powered or matched_power_configuration ): continue - elif ( + + if ( cluster.cluster_id == PowerConfiguration.cluster_id and not zha_device.is_mains_powered ): matched_power_configuration = True + cluster_match_results.append( _async_handle_single_cluster_match( hass, diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 223ce810d7cefe..841b283a98dd1f 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -851,7 +851,8 @@ async def _check_awaked(): # Need to be in STATE_AWAKED before talking to nodes. _LOGGER.info("Z-Wave ready after %d seconds", waited) break - elif waited >= const.NETWORK_READY_WAIT_SECS: + + if waited >= const.NETWORK_READY_WAIT_SECS: # Wait up to NETWORK_READY_WAIT_SECS seconds for the Z-Wave # network to be ready. _LOGGER.warning( @@ -861,8 +862,8 @@ async def _check_awaked(): "final network state: %d %s", network.state, network.state_str ) break - else: - await asyncio.sleep(1) + + await asyncio.sleep(1) hass.async_add_job(_finalize_start) diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index dbfb9ae1864f10..4c1a9803d754a4 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -19,7 +19,8 @@ def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, if not platform_config: continue - elif not isinstance(platform_config, list): + + if not isinstance(platform_config, list): platform_config = [platform_config] for item in platform_config: diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 00f5984c58ba43..ecac61895c53de 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -23,7 +23,8 @@ def run(args: List) -> int: for fil in os.listdir(path): if fil == "__pycache__": continue - elif os.path.isdir(os.path.join(path, fil)): + + if os.path.isdir(os.path.join(path, fil)): scripts.append(fil) elif fil != "__init__.py" and fil.endswith(".py"): scripts.append(fil[:-3]) From 6f9ccb54342fdd723c769724da0f1e9ea98894a1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Sep 2019 23:20:04 +0200 Subject: [PATCH 144/296] Add and corrects typehints in Entity helper & core class (#26805) * Add and corrects typehints in Entity class * Adjust state type based on comments --- homeassistant/core.py | 8 ++++---- homeassistant/helpers/entity.py | 28 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index c29d41ace9a126..31761f2560faf4 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -703,7 +703,7 @@ class State: def __init__( self, entity_id: str, - state: Any, + state: str, attributes: Optional[Dict] = None, last_changed: Optional[datetime.datetime] = None, last_updated: Optional[datetime.datetime] = None, @@ -732,7 +732,7 @@ def __init__( ) self.entity_id = entity_id.lower() - self.state: str = state + self.state = state self.attributes = MappingProxyType(attributes or {}) self.last_updated = last_updated or dt_util.utcnow() self.last_changed = last_changed or self.last_updated @@ -924,7 +924,7 @@ def async_remove(self, entity_id: str) -> bool: def set( self, entity_id: str, - new_state: Any, + new_state: str, attributes: Optional[Dict] = None, force_update: bool = False, context: Optional[Context] = None, @@ -950,7 +950,7 @@ def set( def async_set( self, entity_id: str, - new_state: Any, + new_state: str, attributes: Optional[Dict] = None, force_update: bool = False, context: Optional[Context] = None, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index af8d5589c8a85a..4911c5d5fb9157 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -3,7 +3,7 @@ import logging import functools as ft from timeit import default_timer as timer -from typing import Any, Optional, List, Iterable +from typing import Any, Dict, Iterable, List, Optional, Union from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -26,7 +26,7 @@ EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry, ) -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE +from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Context from homeassistant.config import DATA_CUSTOMIZE from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.util import ensure_unique_string, slugify @@ -137,12 +137,12 @@ def name(self) -> Optional[str]: return None @property - def state(self) -> str: + def state(self) -> Union[None, str, int, float]: """Return the state of the entity.""" return STATE_UNKNOWN @property - def state_attributes(self): + def state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes. Implemented by component base class. @@ -150,7 +150,7 @@ def state_attributes(self): return None @property - def device_state_attributes(self): + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return device specific state attributes. Implemented by platform classes. @@ -158,7 +158,7 @@ def device_state_attributes(self): return None @property - def device_info(self): + def device_info(self) -> Optional[Dict[str, Any]]: """Return device specific attributes. Implemented by platform classes. @@ -171,17 +171,17 @@ def device_class(self) -> Optional[str]: return None @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> Optional[str]: """Return the unit of measurement of this entity, if any.""" return None @property - def icon(self): + def icon(self) -> Optional[str]: """Return the icon to use in the frontend, if any.""" return None @property - def entity_picture(self): + def entity_picture(self) -> Optional[str]: """Return the entity picture to use in the frontend, if any.""" return None @@ -215,12 +215,12 @@ def supported_features(self) -> Optional[int]: return None @property - def context_recent_time(self): + def context_recent_time(self) -> timedelta: """Time that a context is considered recent.""" return timedelta(seconds=5) @property - def entity_registry_enabled_default(self): + def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return True @@ -230,12 +230,12 @@ def entity_registry_enabled_default(self): # produce undesirable effects in the entity's operation. @property - def enabled(self): + def enabled(self) -> bool: """Return if the entity is enabled in the entity registry.""" return self.registry_entry is None or not self.registry_entry.disabled @callback - def async_set_context(self, context): + def async_set_context(self, context: Context) -> None: """Set the context the entity currently operates under.""" self._context = context self._context_set = dt_util.utcnow() @@ -540,7 +540,7 @@ def __eq__(self, other): return self.unique_id == other.unique_id - def __repr__(self): + def __repr__(self) -> str: """Return the representation.""" return "".format(self.name, self.state) From b52cfd34098ef3c271a5a8a67047493f1860f3db Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Sep 2019 23:21:00 +0200 Subject: [PATCH 145/296] Add comment for clarity to helper.entity.enabled() (#26793) * Fixes entity enabled expression * Ensure True is returned when there is no registry_entity * Add comment for clarity to helper.entity.enabled() --- homeassistant/helpers/entity.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 4911c5d5fb9157..fad02dee075839 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -231,7 +231,11 @@ def entity_registry_enabled_default(self) -> bool: @property def enabled(self) -> bool: - """Return if the entity is enabled in the entity registry.""" + """Return if the entity is enabled in the entity registry. + + If an entity is not part of the registry, it cannot be disabled + and will therefore always be enabled. + """ return self.registry_entry is None or not self.registry_entry.disabled @callback From 6fdff9ffab9618fcec3b4d364a3faab86f768440 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 Sep 2019 14:57:05 -0700 Subject: [PATCH 146/296] Reorg device automation (#26880) * async_trigger -> async_attach_trigger * Reorg device automations * Update docstrings * Fix types * Fix extending schemas --- .../components/automation/__init__.py | 9 +- homeassistant/components/automation/device.py | 6 +- homeassistant/components/automation/event.py | 6 +- .../components/automation/geo_location.py | 2 +- .../components/automation/homeassistant.py | 2 +- .../components/automation/litejet.py | 2 +- homeassistant/components/automation/mqtt.py | 2 +- .../components/automation/numeric_state.py | 2 +- homeassistant/components/automation/state.py | 6 +- homeassistant/components/automation/sun.py | 2 +- .../components/automation/template.py | 2 +- homeassistant/components/automation/time.py | 2 +- .../components/automation/time_pattern.py | 2 +- .../components/automation/webhook.py | 2 +- homeassistant/components/automation/zone.py | 2 +- .../binary_sensor/device_automation.py | 423 ------------------ .../binary_sensor/device_condition.py | 247 ++++++++++ .../binary_sensor/device_trigger.py | 238 ++++++++++ ...device_automation.py => device_trigger.py} | 19 +- .../components/device_automation/__init__.py | 68 ++- .../device_automation/toggle_entity.py | 98 ++-- .../components/light/device_action.py | 30 ++ .../components/light/device_automation.py | 56 --- .../components/light/device_condition.py | 28 ++ .../components/light/device_trigger.py | 33 ++ .../components/switch/device_action.py | 30 ++ .../components/switch/device_automation.py | 56 --- .../components/switch/device_condition.py | 28 ++ .../components/switch/device_trigger.py | 33 ++ ...device_automation.py => device_trigger.py} | 19 +- homeassistant/helpers/condition.py | 46 +- homeassistant/helpers/config_validation.py | 18 +- homeassistant/helpers/script.py | 2 +- tests/common.py | 4 +- .../binary_sensor/test_device_automation.py | 309 ------------- .../binary_sensor/test_device_condition.py | 144 ++++++ .../binary_sensor/test_device_trigger.py | 154 +++++++ .../{test_binary_sensor.py => test_init.py} | 0 ...e_automation.py => test_device_trigger.py} | 44 +- tests/components/light/test_device_action.py | 140 ++++++ .../light/test_device_automation.py | 373 --------------- .../components/light/test_device_condition.py | 136 ++++++ tests/components/light/test_device_trigger.py | 147 ++++++ tests/components/switch/test_device_action.py | 142 ++++++ .../switch/test_device_automation.py | 373 --------------- .../switch/test_device_condition.py | 138 ++++++ .../components/switch/test_device_trigger.py | 147 ++++++ .../components/zha/test_device_automation.py | 13 +- 48 files changed, 2014 insertions(+), 1771 deletions(-) delete mode 100644 homeassistant/components/binary_sensor/device_automation.py create mode 100644 homeassistant/components/binary_sensor/device_condition.py create mode 100644 homeassistant/components/binary_sensor/device_trigger.py rename homeassistant/components/deconz/{device_automation.py => device_trigger.py} (94%) create mode 100644 homeassistant/components/light/device_action.py delete mode 100644 homeassistant/components/light/device_automation.py create mode 100644 homeassistant/components/light/device_condition.py create mode 100644 homeassistant/components/light/device_trigger.py create mode 100644 homeassistant/components/switch/device_action.py delete mode 100644 homeassistant/components/switch/device_automation.py create mode 100644 homeassistant/components/switch/device_condition.py create mode 100644 homeassistant/components/switch/device_trigger.py rename homeassistant/components/zha/{device_automation.py => device_trigger.py} (84%) delete mode 100644 tests/components/binary_sensor/test_device_automation.py create mode 100644 tests/components/binary_sensor/test_device_condition.py create mode 100644 tests/components/binary_sensor/test_device_trigger.py rename tests/components/binary_sensor/{test_binary_sensor.py => test_init.py} (100%) rename tests/components/deconz/{test_device_automation.py => test_device_trigger.py} (71%) create mode 100644 tests/components/light/test_device_action.py delete mode 100644 tests/components/light/test_device_automation.py create mode 100644 tests/components/light/test_device_condition.py create mode 100644 tests/components/light/test_device_trigger.py create mode 100644 tests/components/switch/test_device_action.py delete mode 100644 tests/components/switch/test_device_automation.py create mode 100644 tests/components/switch/test_device_condition.py create mode 100644 tests/components/switch/test_device_trigger.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f0529f126f1e73..f669d415854b9d 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -3,7 +3,7 @@ from functools import partial import importlib import logging -from typing import Any +from typing import Any, Awaitable, Callable import voluptuous as vol @@ -23,7 +23,7 @@ SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Context, CoreState +from homeassistant.core import Context, CoreState, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, extract_domain_configs, script import homeassistant.helpers.config_validation as cv @@ -31,6 +31,7 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime, utcnow @@ -67,6 +68,8 @@ _LOGGER = logging.getLogger(__name__) +AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]] + def _platform_validator(config): """Validate it is a valid platform.""" @@ -474,7 +477,7 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action): platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__) try: - remove = await platform.async_trigger(hass, conf, action, info) + remove = await platform.async_attach_trigger(hass, conf, action, info) except InvalidDeviceAutomationConfig: remove = False diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index b090484ab6785d..fe2d65edef616b 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -13,8 +13,8 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for trigger.""" integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_automation") - return await platform.async_trigger(hass, config, action, automation_info) + platform = integration.get_platform("device_trigger") + return await platform.async_attach_trigger(hass, config, action, automation_info) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index d372aedd1d74f3..26dacac974d559 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -24,7 +24,9 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="event" +): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) event_data_schema = ( @@ -47,7 +49,7 @@ def handle_event(event): hass.async_run_job( action( - {"trigger": {"platform": "event", "event": event}}, + {"trigger": {"platform": platform_type, "event": event}}, context=event.context, ) ) diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index 3f2aa1c00d794a..0ef0884d329e64 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -37,7 +37,7 @@ def source_match(state, source): return state and state.attributes.get("source") == source -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" source = config.get(CONF_SOURCE).lower() zone_entity_id = config.get(CONF_ZONE) diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index bd1da7e7e1f35a..e4eb029d5aa778 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -21,7 +21,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 7bc4c9377656c5..9512db8261dbbf 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -32,7 +32,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" number = config.get(CONF_NUMBER) held_more_than = config.get(CONF_HELD_MORE_THAN) diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index fd9a778dbfc454..135a421f72e1fb 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -25,7 +25,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" topic = config[CONF_TOPIC] payload = config.get(CONF_PAYLOAD) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index b33d724d7700fe..9dd4657291dc21 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -40,7 +40,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 5fbe97185a7fa5..184b9ea302b11c 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -37,7 +37,9 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="state" +): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) @@ -59,7 +61,7 @@ def call_action(): action( { "trigger": { - "platform": "state", + "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 7cbbe56f326b97..66892784a54d1d 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -28,7 +28,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index c83d660912cf53..f2b4134de42282 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -28,7 +28,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" value_template = config.get(CONF_VALUE_TEMPLATE) value_template.hass = hass diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 3942d0efadbb9d..231bc346e141c0 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -18,7 +18,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" at_time = config.get(CONF_AT) hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second diff --git a/homeassistant/components/automation/time_pattern.py b/homeassistant/components/automation/time_pattern.py index f749a308bf7a31..ee092916112024 100644 --- a/homeassistant/components/automation/time_pattern.py +++ b/homeassistant/components/automation/time_pattern.py @@ -30,7 +30,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" hours = config.get(CONF_HOURS) minutes = config.get(CONF_MINUTES) diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index 706afbe90421f2..bbcf9bd9ddcad2 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -36,7 +36,7 @@ async def _handle_webhook(action, hass, webhook_id, request): hass.async_run_job(action, {"trigger": result}) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Trigger based on incoming webhooks.""" webhook_id = config.get(CONF_WEBHOOK_ID) hass.components.webhook.async_register( diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 35b110060244f7..535ef298a2a78a 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -31,7 +31,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) diff --git a/homeassistant/components/binary_sensor/device_automation.py b/homeassistant/components/binary_sensor/device_automation.py deleted file mode 100644 index c609c2eb5da4c8..00000000000000 --- a/homeassistant/components/binary_sensor/device_automation.py +++ /dev/null @@ -1,423 +0,0 @@ -"""Provides device automations for lights.""" -import voluptuous as vol - -import homeassistant.components.automation.state as state -from homeassistant.components.device_automation.const import ( - CONF_IS_OFF, - CONF_IS_ON, - CONF_TURNED_OFF, - CONF_TURNED_ON, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - CONF_CONDITION, - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_ENTITY_ID, - CONF_PLATFORM, - CONF_TYPE, -) -from homeassistant.core import split_entity_id -from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers import condition, config_validation as cv - -from . import ( - DOMAIN, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_COLD, - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GARAGE_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HEAT, - DEVICE_CLASS_LIGHT, - DEVICE_CLASS_LOCK, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_MOVING, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_PLUG, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESENCE, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_SOUND, - DEVICE_CLASS_VIBRATION, - DEVICE_CLASS_WINDOW, -) - - -# mypy: allow-untyped-defs, no-check-untyped-defs - -DEVICE_CLASS_NONE = "none" - -CONF_IS_BAT_LOW = "is_bat_low" -CONF_IS_NOT_BAT_LOW = "is_not_bat_low" -CONF_IS_COLD = "is_cold" -CONF_IS_NOT_COLD = "is_not_cold" -CONF_IS_CONNECTED = "is_connected" -CONF_IS_NOT_CONNECTED = "is_not_connected" -CONF_IS_GAS = "is_gas" -CONF_IS_NO_GAS = "is_no_gas" -CONF_IS_HOT = "is_hot" -CONF_IS_NOT_HOT = "is_not_hot" -CONF_IS_LIGHT = "is_light" -CONF_IS_NO_LIGHT = "is_no_light" -CONF_IS_LOCKED = "is_locked" -CONF_IS_NOT_LOCKED = "is_not_locked" -CONF_IS_MOIST = "is_moist" -CONF_IS_NOT_MOIST = "is_not_moist" -CONF_IS_MOTION = "is_motion" -CONF_IS_NO_MOTION = "is_no_motion" -CONF_IS_MOVING = "is_moving" -CONF_IS_NOT_MOVING = "is_not_moving" -CONF_IS_OCCUPIED = "is_occupied" -CONF_IS_NOT_OCCUPIED = "is_not_occupied" -CONF_IS_PLUGGED_IN = "is_plugged_in" -CONF_IS_NOT_PLUGGED_IN = "is_not_plugged_in" -CONF_IS_POWERED = "is_powered" -CONF_IS_NOT_POWERED = "is_not_powered" -CONF_IS_PRESENT = "is_present" -CONF_IS_NOT_PRESENT = "is_not_present" -CONF_IS_PROBLEM = "is_problem" -CONF_IS_NO_PROBLEM = "is_no_problem" -CONF_IS_UNSAFE = "is_unsafe" -CONF_IS_NOT_UNSAFE = "is_not_unsafe" -CONF_IS_SMOKE = "is_smoke" -CONF_IS_NO_SMOKE = "is_no_smoke" -CONF_IS_SOUND = "is_sound" -CONF_IS_NO_SOUND = "is_no_sound" -CONF_IS_VIBRATION = "is_vibration" -CONF_IS_NO_VIBRATION = "is_no_vibration" -CONF_IS_OPEN = "is_open" -CONF_IS_NOT_OPEN = "is_not_open" - -CONF_BAT_LOW = "bat_low" -CONF_NOT_BAT_LOW = "not_bat_low" -CONF_COLD = "cold" -CONF_NOT_COLD = "not_cold" -CONF_CONNECTED = "connected" -CONF_NOT_CONNECTED = "not_connected" -CONF_GAS = "gas" -CONF_NO_GAS = "no_gas" -CONF_HOT = "hot" -CONF_NOT_HOT = "not_hot" -CONF_LIGHT = "light" -CONF_NO_LIGHT = "no_light" -CONF_LOCKED = "locked" -CONF_NOT_LOCKED = "not_locked" -CONF_MOIST = "moist" -CONF_NOT_MOIST = "not_moist" -CONF_MOTION = "motion" -CONF_NO_MOTION = "no_motion" -CONF_MOVING = "moving" -CONF_NOT_MOVING = "not_moving" -CONF_OCCUPIED = "occupied" -CONF_NOT_OCCUPIED = "not_occupied" -CONF_PLUGGED_IN = "plugged_in" -CONF_NOT_PLUGGED_IN = "not_plugged_in" -CONF_POWERED = "powered" -CONF_NOT_POWERED = "not_powered" -CONF_PRESENT = "present" -CONF_NOT_PRESENT = "not_present" -CONF_PROBLEM = "problem" -CONF_NO_PROBLEM = "no_problem" -CONF_UNSAFE = "unsafe" -CONF_NOT_UNSAFE = "not_unsafe" -CONF_SMOKE = "smoke" -CONF_NO_SMOKE = "no_smoke" -CONF_SOUND = "sound" -CONF_NO_SOUND = "no_sound" -CONF_VIBRATION = "vibration" -CONF_NO_VIBRATION = "no_vibration" -CONF_OPEN = "open" -CONF_NOT_OPEN = "not_open" - -IS_ON = [ - CONF_IS_BAT_LOW, - CONF_IS_COLD, - CONF_IS_CONNECTED, - CONF_IS_GAS, - CONF_IS_HOT, - CONF_IS_LIGHT, - CONF_IS_LOCKED, - CONF_IS_MOIST, - CONF_IS_MOTION, - CONF_IS_MOVING, - CONF_IS_OCCUPIED, - CONF_IS_OPEN, - CONF_IS_PLUGGED_IN, - CONF_IS_POWERED, - CONF_IS_PRESENT, - CONF_IS_PROBLEM, - CONF_IS_SMOKE, - CONF_IS_SOUND, - CONF_IS_UNSAFE, - CONF_IS_VIBRATION, - CONF_IS_ON, -] - -IS_OFF = [ - CONF_IS_NOT_BAT_LOW, - CONF_IS_NOT_COLD, - CONF_IS_NOT_CONNECTED, - CONF_IS_NOT_HOT, - CONF_IS_NOT_LOCKED, - CONF_IS_NOT_MOIST, - CONF_IS_NOT_MOVING, - CONF_IS_NOT_OCCUPIED, - CONF_IS_NOT_OPEN, - CONF_IS_NOT_PLUGGED_IN, - CONF_IS_NOT_POWERED, - CONF_IS_NOT_PRESENT, - CONF_IS_NOT_UNSAFE, - CONF_IS_NO_GAS, - CONF_IS_NO_LIGHT, - CONF_IS_NO_MOTION, - CONF_IS_NO_PROBLEM, - CONF_IS_NO_SMOKE, - CONF_IS_NO_SOUND, - CONF_IS_NO_VIBRATION, - CONF_IS_OFF, -] - -TURNED_ON = [ - CONF_BAT_LOW, - CONF_COLD, - CONF_CONNECTED, - CONF_GAS, - CONF_HOT, - CONF_LIGHT, - CONF_LOCKED, - CONF_MOIST, - CONF_MOTION, - CONF_MOVING, - CONF_OCCUPIED, - CONF_OPEN, - CONF_PLUGGED_IN, - CONF_POWERED, - CONF_PRESENT, - CONF_PROBLEM, - CONF_SMOKE, - CONF_SOUND, - CONF_UNSAFE, - CONF_VIBRATION, - CONF_TURNED_ON, -] - -TURNED_OFF = [ - CONF_NOT_BAT_LOW, - CONF_NOT_COLD, - CONF_NOT_CONNECTED, - CONF_NOT_HOT, - CONF_NOT_LOCKED, - CONF_NOT_MOIST, - CONF_NOT_MOVING, - CONF_NOT_OCCUPIED, - CONF_NOT_OPEN, - CONF_NOT_PLUGGED_IN, - CONF_NOT_POWERED, - CONF_NOT_PRESENT, - CONF_NOT_UNSAFE, - CONF_NO_GAS, - CONF_NO_LIGHT, - CONF_NO_MOTION, - CONF_NO_PROBLEM, - CONF_NO_SMOKE, - CONF_NO_SOUND, - CONF_NO_VIBRATION, - CONF_TURNED_OFF, -] - -ENTITY_CONDITIONS = { - DEVICE_CLASS_BATTERY: [ - {CONF_TYPE: CONF_IS_BAT_LOW}, - {CONF_TYPE: CONF_IS_NOT_BAT_LOW}, - ], - DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_IS_COLD}, {CONF_TYPE: CONF_IS_NOT_COLD}], - DEVICE_CLASS_CONNECTIVITY: [ - {CONF_TYPE: CONF_IS_CONNECTED}, - {CONF_TYPE: CONF_IS_NOT_CONNECTED}, - ], - DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], - DEVICE_CLASS_GARAGE_DOOR: [ - {CONF_TYPE: CONF_IS_OPEN}, - {CONF_TYPE: CONF_IS_NOT_OPEN}, - ], - DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}, {CONF_TYPE: CONF_IS_NO_GAS}], - DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_IS_HOT}, {CONF_TYPE: CONF_IS_NOT_HOT}], - DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_IS_LIGHT}, {CONF_TYPE: CONF_IS_NO_LIGHT}], - DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_IS_LOCKED}, {CONF_TYPE: CONF_IS_NOT_LOCKED}], - DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_IS_MOIST}, {CONF_TYPE: CONF_IS_NOT_MOIST}], - DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_IS_MOTION}, {CONF_TYPE: CONF_IS_NO_MOTION}], - DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_IS_MOVING}, {CONF_TYPE: CONF_IS_NOT_MOVING}], - DEVICE_CLASS_OCCUPANCY: [ - {CONF_TYPE: CONF_IS_OCCUPIED}, - {CONF_TYPE: CONF_IS_NOT_OCCUPIED}, - ], - DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], - DEVICE_CLASS_PLUG: [ - {CONF_TYPE: CONF_IS_PLUGGED_IN}, - {CONF_TYPE: CONF_IS_NOT_PLUGGED_IN}, - ], - DEVICE_CLASS_POWER: [ - {CONF_TYPE: CONF_IS_POWERED}, - {CONF_TYPE: CONF_IS_NOT_POWERED}, - ], - DEVICE_CLASS_PRESENCE: [ - {CONF_TYPE: CONF_IS_PRESENT}, - {CONF_TYPE: CONF_IS_NOT_PRESENT}, - ], - DEVICE_CLASS_PROBLEM: [ - {CONF_TYPE: CONF_IS_PROBLEM}, - {CONF_TYPE: CONF_IS_NO_PROBLEM}, - ], - DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], - DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], - DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], - DEVICE_CLASS_VIBRATION: [ - {CONF_TYPE: CONF_IS_VIBRATION}, - {CONF_TYPE: CONF_IS_NO_VIBRATION}, - ], - DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], - DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_ON}, {CONF_TYPE: CONF_IS_OFF}], -} - -ENTITY_TRIGGERS = { - DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BAT_LOW}, {CONF_TYPE: CONF_NOT_BAT_LOW}], - DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_COLD}, {CONF_TYPE: CONF_NOT_COLD}], - DEVICE_CLASS_CONNECTIVITY: [ - {CONF_TYPE: CONF_CONNECTED}, - {CONF_TYPE: CONF_NOT_CONNECTED}, - ], - DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], - DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], - DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], - DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}], - DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_MOIST}, {CONF_TYPE: CONF_NOT_MOIST}], - DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_MOTION}, {CONF_TYPE: CONF_NO_MOTION}], - DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_MOVING}, {CONF_TYPE: CONF_NOT_MOVING}], - DEVICE_CLASS_OCCUPANCY: [ - {CONF_TYPE: CONF_OCCUPIED}, - {CONF_TYPE: CONF_NOT_OCCUPIED}, - ], - DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], - DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], - DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], - DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}], - DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], - DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], - DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], - DEVICE_CLASS_VIBRATION: [ - {CONF_TYPE: CONF_VIBRATION}, - {CONF_TYPE: CONF_NO_VIBRATION}, - ], - DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], -} - -CONDITION_SCHEMA = vol.Schema( - { - vol.Required(CONF_CONDITION): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), - } -) - -TRIGGER_SCHEMA = vol.Schema( - { - vol.Required(CONF_PLATFORM): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), - } -) - - -def async_condition_from_config(config, config_validation): - """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) - condition_type = config[CONF_TYPE] - if condition_type in IS_ON: - stat = "on" - else: - stat = "off" - state_config = { - condition.CONF_CONDITION: "state", - condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - condition.CONF_STATE: stat, - } - - return condition.state_from_config(state_config, config_validation) - - -async def async_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - trigger_type = config[CONF_TYPE] - if trigger_type in TURNED_ON: - from_state = "off" - to_state = "on" - else: - from_state = "on" - to_state = "off" - state_config = { - state.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, - state.CONF_TO: to_state, - } - - return await state.async_trigger(hass, state_config, action, automation_info) - - -def _is_domain(entity, domain): - return split_entity_id(entity.entity_id)[0] == domain - - -async def _async_get_automations(hass, device_id, automation_templates, domain): - """List device automations.""" - automations = [] - entity_registry = await hass.helpers.entity_registry.async_get_registry() - - entities = async_entries_for_device(entity_registry, device_id) - domain_entities = [x for x in entities if _is_domain(x, domain)] - for entity in domain_entities: - device_class = DEVICE_CLASS_NONE - entity_id = entity.entity_id - entity = hass.states.get(entity_id) - if entity and ATTR_DEVICE_CLASS in entity.attributes: - device_class = entity.attributes[ATTR_DEVICE_CLASS] - automation_template = automation_templates[device_class] - - for automation in automation_template: - automation = dict(automation) - automation.update(device_id=device_id, entity_id=entity_id, domain=domain) - automations.append(automation) - - return automations - - -async def async_get_conditions(hass, device_id): - """List device conditions.""" - automations = await _async_get_automations( - hass, device_id, ENTITY_CONDITIONS, DOMAIN - ) - for automation in automations: - automation.update(condition="device") - return automations - - -async def async_get_triggers(hass, device_id): - """List device triggers.""" - automations = await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, DOMAIN) - for automation in automations: - automation.update(platform="device") - return automations diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py new file mode 100644 index 00000000000000..70b79becb8b12d --- /dev/null +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -0,0 +1,247 @@ +"""Implemenet device conditions for binary sensor.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.entity_registry import ( + async_entries_for_device, + async_get_registry, +) +from homeassistant.helpers.typing import ConfigType + +from . import ( + DOMAIN, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, +) + +DEVICE_CLASS_NONE = "none" + +CONF_IS_BAT_LOW = "is_bat_low" +CONF_IS_NOT_BAT_LOW = "is_not_bat_low" +CONF_IS_COLD = "is_cold" +CONF_IS_NOT_COLD = "is_not_cold" +CONF_IS_CONNECTED = "is_connected" +CONF_IS_NOT_CONNECTED = "is_not_connected" +CONF_IS_GAS = "is_gas" +CONF_IS_NO_GAS = "is_no_gas" +CONF_IS_HOT = "is_hot" +CONF_IS_NOT_HOT = "is_not_hot" +CONF_IS_LIGHT = "is_light" +CONF_IS_NO_LIGHT = "is_no_light" +CONF_IS_LOCKED = "is_locked" +CONF_IS_NOT_LOCKED = "is_not_locked" +CONF_IS_MOIST = "is_moist" +CONF_IS_NOT_MOIST = "is_not_moist" +CONF_IS_MOTION = "is_motion" +CONF_IS_NO_MOTION = "is_no_motion" +CONF_IS_MOVING = "is_moving" +CONF_IS_NOT_MOVING = "is_not_moving" +CONF_IS_OCCUPIED = "is_occupied" +CONF_IS_NOT_OCCUPIED = "is_not_occupied" +CONF_IS_PLUGGED_IN = "is_plugged_in" +CONF_IS_NOT_PLUGGED_IN = "is_not_plugged_in" +CONF_IS_POWERED = "is_powered" +CONF_IS_NOT_POWERED = "is_not_powered" +CONF_IS_PRESENT = "is_present" +CONF_IS_NOT_PRESENT = "is_not_present" +CONF_IS_PROBLEM = "is_problem" +CONF_IS_NO_PROBLEM = "is_no_problem" +CONF_IS_UNSAFE = "is_unsafe" +CONF_IS_NOT_UNSAFE = "is_not_unsafe" +CONF_IS_SMOKE = "is_smoke" +CONF_IS_NO_SMOKE = "is_no_smoke" +CONF_IS_SOUND = "is_sound" +CONF_IS_NO_SOUND = "is_no_sound" +CONF_IS_VIBRATION = "is_vibration" +CONF_IS_NO_VIBRATION = "is_no_vibration" +CONF_IS_OPEN = "is_open" +CONF_IS_NOT_OPEN = "is_not_open" + +IS_ON = [ + CONF_IS_BAT_LOW, + CONF_IS_COLD, + CONF_IS_CONNECTED, + CONF_IS_GAS, + CONF_IS_HOT, + CONF_IS_LIGHT, + CONF_IS_LOCKED, + CONF_IS_MOIST, + CONF_IS_MOTION, + CONF_IS_MOVING, + CONF_IS_OCCUPIED, + CONF_IS_OPEN, + CONF_IS_PLUGGED_IN, + CONF_IS_POWERED, + CONF_IS_PRESENT, + CONF_IS_PROBLEM, + CONF_IS_SMOKE, + CONF_IS_SOUND, + CONF_IS_UNSAFE, + CONF_IS_VIBRATION, + CONF_IS_ON, +] + +IS_OFF = [ + CONF_IS_NOT_BAT_LOW, + CONF_IS_NOT_COLD, + CONF_IS_NOT_CONNECTED, + CONF_IS_NOT_HOT, + CONF_IS_NOT_LOCKED, + CONF_IS_NOT_MOIST, + CONF_IS_NOT_MOVING, + CONF_IS_NOT_OCCUPIED, + CONF_IS_NOT_OPEN, + CONF_IS_NOT_PLUGGED_IN, + CONF_IS_NOT_POWERED, + CONF_IS_NOT_PRESENT, + CONF_IS_NOT_UNSAFE, + CONF_IS_NO_GAS, + CONF_IS_NO_LIGHT, + CONF_IS_NO_MOTION, + CONF_IS_NO_PROBLEM, + CONF_IS_NO_SMOKE, + CONF_IS_NO_SOUND, + CONF_IS_NO_VIBRATION, + CONF_IS_OFF, +] + +ENTITY_CONDITIONS = { + DEVICE_CLASS_BATTERY: [ + {CONF_TYPE: CONF_IS_BAT_LOW}, + {CONF_TYPE: CONF_IS_NOT_BAT_LOW}, + ], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_IS_COLD}, {CONF_TYPE: CONF_IS_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_IS_CONNECTED}, + {CONF_TYPE: CONF_IS_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_GARAGE_DOOR: [ + {CONF_TYPE: CONF_IS_OPEN}, + {CONF_TYPE: CONF_IS_NOT_OPEN}, + ], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}, {CONF_TYPE: CONF_IS_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_IS_HOT}, {CONF_TYPE: CONF_IS_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_IS_LIGHT}, {CONF_TYPE: CONF_IS_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_IS_LOCKED}, {CONF_TYPE: CONF_IS_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_IS_MOIST}, {CONF_TYPE: CONF_IS_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_IS_MOTION}, {CONF_TYPE: CONF_IS_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_IS_MOVING}, {CONF_TYPE: CONF_IS_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_IS_OCCUPIED}, + {CONF_TYPE: CONF_IS_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_PLUG: [ + {CONF_TYPE: CONF_IS_PLUGGED_IN}, + {CONF_TYPE: CONF_IS_NOT_PLUGGED_IN}, + ], + DEVICE_CLASS_POWER: [ + {CONF_TYPE: CONF_IS_POWERED}, + {CONF_TYPE: CONF_IS_NOT_POWERED}, + ], + DEVICE_CLASS_PRESENCE: [ + {CONF_TYPE: CONF_IS_PRESENT}, + {CONF_TYPE: CONF_IS_NOT_PRESENT}, + ], + DEVICE_CLASS_PROBLEM: [ + {CONF_TYPE: CONF_IS_PROBLEM}, + {CONF_TYPE: CONF_IS_NO_PROBLEM}, + ], + DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], + DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], + DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], + DEVICE_CLASS_VIBRATION: [ + {CONF_TYPE: CONF_IS_VIBRATION}, + {CONF_TYPE: CONF_IS_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_ON}, {CONF_TYPE: CONF_IS_OFF}], +} + +CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), + } +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + conditions: List[dict] = [] + entity_registry = await async_get_registry(hass) + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + if state and ATTR_DEVICE_CLASS in state.attributes: + device_class = state.attributes[ATTR_DEVICE_CLASS] + + templates = ENTITY_CONDITIONS.get( + device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE] + ) + + conditions.extend( + ( + { + **template, + "condition": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for template in templates + ) + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + condition_type = config[CONF_TYPE] + if condition_type in IS_ON: + stat = "on" + else: + stat = "off" + state_config = { + condition.CONF_CONDITION: "state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + condition.CONF_STATE: stat, + } + + return condition.state_from_config(state_config, config_validation) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py new file mode 100644 index 00000000000000..2211b3001045ed --- /dev/null +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -0,0 +1,238 @@ +"""Provides device triggers for binary sensors.""" +import voluptuous as vol + +from homeassistant.components.automation import state as state_automation +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.const import ( + CONF_TURNED_OFF, + CONF_TURNED_ON, +) +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import config_validation as cv + +from . import ( + DOMAIN, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, +) + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_BAT_LOW = "bat_low" +CONF_NOT_BAT_LOW = "not_bat_low" +CONF_COLD = "cold" +CONF_NOT_COLD = "not_cold" +CONF_CONNECTED = "connected" +CONF_NOT_CONNECTED = "not_connected" +CONF_GAS = "gas" +CONF_NO_GAS = "no_gas" +CONF_HOT = "hot" +CONF_NOT_HOT = "not_hot" +CONF_LIGHT = "light" +CONF_NO_LIGHT = "no_light" +CONF_LOCKED = "locked" +CONF_NOT_LOCKED = "not_locked" +CONF_MOIST = "moist" +CONF_NOT_MOIST = "not_moist" +CONF_MOTION = "motion" +CONF_NO_MOTION = "no_motion" +CONF_MOVING = "moving" +CONF_NOT_MOVING = "not_moving" +CONF_OCCUPIED = "occupied" +CONF_NOT_OCCUPIED = "not_occupied" +CONF_PLUGGED_IN = "plugged_in" +CONF_NOT_PLUGGED_IN = "not_plugged_in" +CONF_POWERED = "powered" +CONF_NOT_POWERED = "not_powered" +CONF_PRESENT = "present" +CONF_NOT_PRESENT = "not_present" +CONF_PROBLEM = "problem" +CONF_NO_PROBLEM = "no_problem" +CONF_UNSAFE = "unsafe" +CONF_NOT_UNSAFE = "not_unsafe" +CONF_SMOKE = "smoke" +CONF_NO_SMOKE = "no_smoke" +CONF_SOUND = "sound" +CONF_NO_SOUND = "no_sound" +CONF_VIBRATION = "vibration" +CONF_NO_VIBRATION = "no_vibration" +CONF_OPEN = "open" +CONF_NOT_OPEN = "not_open" + + +TURNED_ON = [ + CONF_BAT_LOW, + CONF_COLD, + CONF_CONNECTED, + CONF_GAS, + CONF_HOT, + CONF_LIGHT, + CONF_LOCKED, + CONF_MOIST, + CONF_MOTION, + CONF_MOVING, + CONF_OCCUPIED, + CONF_OPEN, + CONF_PLUGGED_IN, + CONF_POWERED, + CONF_PRESENT, + CONF_PROBLEM, + CONF_SMOKE, + CONF_SOUND, + CONF_UNSAFE, + CONF_VIBRATION, + CONF_TURNED_ON, +] + +TURNED_OFF = [ + CONF_NOT_BAT_LOW, + CONF_NOT_COLD, + CONF_NOT_CONNECTED, + CONF_NOT_HOT, + CONF_NOT_LOCKED, + CONF_NOT_MOIST, + CONF_NOT_MOVING, + CONF_NOT_OCCUPIED, + CONF_NOT_OPEN, + CONF_NOT_PLUGGED_IN, + CONF_NOT_POWERED, + CONF_NOT_PRESENT, + CONF_NOT_UNSAFE, + CONF_NO_GAS, + CONF_NO_LIGHT, + CONF_NO_MOTION, + CONF_NO_PROBLEM, + CONF_NO_SMOKE, + CONF_NO_SOUND, + CONF_NO_VIBRATION, + CONF_TURNED_OFF, +] + + +ENTITY_TRIGGERS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BAT_LOW}, {CONF_TYPE: CONF_NOT_BAT_LOW}], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_COLD}, {CONF_TYPE: CONF_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_CONNECTED}, + {CONF_TYPE: CONF_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_MOIST}, {CONF_TYPE: CONF_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_MOTION}, {CONF_TYPE: CONF_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_MOVING}, {CONF_TYPE: CONF_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_OCCUPIED}, + {CONF_TYPE: CONF_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], + DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], + DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}], + DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], + DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], + DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], + DEVICE_CLASS_VIBRATION: [ + {CONF_TYPE: CONF_VIBRATION}, + {CONF_TYPE: CONF_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], +} + + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), + } +) + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + trigger_type = config[CONF_TYPE] + if trigger_type in TURNED_ON: + from_state = "off" + to_state = "on" + else: + from_state = "on" + to_state = "off" + + state_config = { + state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_automation.CONF_FROM: from_state, + state_automation.CONF_TO: to_state, + } + + return await state_automation.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + triggers = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = None + state = hass.states.get(entry.entity_id) + if state: + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + templates = ENTITY_TRIGGERS.get( + device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] + ) + + triggers.extend( + ( + { + **automation, + "platform": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for automation in templates + ) + ) + + return triggers diff --git a/homeassistant/components/deconz/device_automation.py b/homeassistant/components/deconz/device_trigger.py similarity index 94% rename from homeassistant/components/deconz/device_automation.py rename to homeassistant/components/deconz/device_trigger.py index 28f36b8f431ea8..77efc78562a924 100644 --- a/homeassistant/components/deconz/device_automation.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -3,6 +3,7 @@ import homeassistant.components.automation.event as event +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -171,16 +172,8 @@ AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, } -TRIGGER_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_PLATFORM): "device", - vol.Required(CONF_TYPE): str, - vol.Required(CONF_SUBTYPE): str, - } - ) +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} ) @@ -198,7 +191,7 @@ def _get_deconz_event_from_device_id(hass, device_id): return None -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" config = TRIGGER_SCHEMA(config) @@ -223,7 +216,9 @@ async def async_trigger(hass, config, action, automation_info): event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, } - return await event.async_trigger(hass, state_config, action, automation_info) + return await event.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) async def async_get_triggers(hass, device_id): diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 9508dd9c849a93..b444abd5238db4 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,16 +1,12 @@ """Helpers for device automations.""" import asyncio import logging -from typing import Callable, cast import voluptuous as vol +from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID from homeassistant.components import websocket_api -from homeassistant.const import CONF_DOMAIN -from homeassistant.core import split_entity_id, HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration, IntegrationNotFound DOMAIN = "device_automation" @@ -18,6 +14,21 @@ _LOGGER = logging.getLogger(__name__) +TRIGGER_BASE_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "device", + vol.Required(CONF_DOMAIN): str, + vol.Required(CONF_DEVICE_ID): str, + } +) + +TYPES = { + "trigger": ("device_trigger", "async_get_triggers"), + "condition": ("device_condition", "async_get_conditions"), + "action": ("device_action", "async_get_actions"), +} + + async def async_setup(hass, config): """Set up device automation.""" hass.components.websocket_api.async_register_command( @@ -32,21 +43,9 @@ async def async_setup(hass, config): return True -async def async_device_condition_from_config( - hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: - """Wrap action method with state based condition.""" - if config_validation: - config = cv.DEVICE_CONDITION_SCHEMA(config) - integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_automation") - return cast( - Callable[..., bool], - platform.async_condition_from_config(config, config_validation), # type: ignore - ) - - -async def _async_get_device_automations_from_domain(hass, domain, fname, device_id): +async def _async_get_device_automations_from_domain( + hass, domain, automation_type, device_id +): """List device automations.""" integration = None try: @@ -55,17 +54,18 @@ async def _async_get_device_automations_from_domain(hass, domain, fname, device_ _LOGGER.warning("Integration %s not found", domain) return None + platform_name, function_name = TYPES[automation_type] + try: - platform = integration.get_platform("device_automation") + platform = integration.get_platform(platform_name) except ImportError: # The domain does not have device automations return None - if hasattr(platform, fname): - return await getattr(platform, fname)(hass, device_id) + return await getattr(platform, function_name)(hass, device_id) -async def _async_get_device_automations(hass, fname, device_id): +async def _async_get_device_automations(hass, automation_type, device_id): """List device automations.""" device_registry, entity_registry = await asyncio.gather( hass.helpers.device_registry.async_get_registry(), @@ -79,13 +79,15 @@ async def _async_get_device_automations(hass, fname, device_id): config_entry = hass.config_entries.async_get_entry(entry_id) domains.add(config_entry.domain) - entities = async_entries_for_device(entity_registry, device_id) - for entity in entities: - domains.add(split_entity_id(entity.entity_id)[0]) + entity_entries = async_entries_for_device(entity_registry, device_id) + for entity_entry in entity_entries: + domains.add(entity_entry.domain) device_automations = await asyncio.gather( *( - _async_get_device_automations_from_domain(hass, domain, fname, device_id) + _async_get_device_automations_from_domain( + hass, domain, automation_type, device_id + ) for domain in domains ) ) @@ -106,7 +108,7 @@ async def _async_get_device_automations(hass, fname, device_id): async def websocket_device_automation_list_actions(hass, connection, msg): """Handle request for device actions.""" device_id = msg["device_id"] - actions = await _async_get_device_automations(hass, "async_get_actions", device_id) + actions = await _async_get_device_automations(hass, "action", device_id) connection.send_result(msg["id"], actions) @@ -120,9 +122,7 @@ async def websocket_device_automation_list_actions(hass, connection, msg): async def websocket_device_automation_list_conditions(hass, connection, msg): """Handle request for device conditions.""" device_id = msg["device_id"] - conditions = await _async_get_device_automations( - hass, "async_get_conditions", device_id - ) + conditions = await _async_get_device_automations(hass, "condition", device_id) connection.send_result(msg["id"], conditions) @@ -136,7 +136,5 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] - triggers = await _async_get_device_automations( - hass, "async_get_triggers", device_id - ) + triggers = await _async_get_device_automations(hass, "trigger", device_id) connection.send_result(msg["id"], triggers) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 1593e70771aea3..b7cadd1349a3eb 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -1,7 +1,9 @@ """Device automation helpers for toggle entity.""" +from typing import List import voluptuous as vol -import homeassistant.components.automation.state as state +from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE +from homeassistant.components.automation import state, AutomationActionType from homeassistant.components.device_automation.const import ( CONF_IS_OFF, CONF_IS_ON, @@ -11,17 +13,11 @@ CONF_TURNED_OFF, CONF_TURNED_ON, ) -from homeassistant.core import split_entity_id -from homeassistant.const import ( - CONF_CONDITION, - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_ENTITY_ID, - CONF_PLATFORM, - CONF_TYPE, -) +from homeassistant.const import CONF_CONDITION, CONF_ENTITY_ID, CONF_PLATFORM, CONF_TYPE from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import condition, config_validation as cv, service +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from . import TRIGGER_BASE_SCHEMA ENTITY_ACTIONS = [ { @@ -64,41 +60,35 @@ }, ] -ACTION_SCHEMA = vol.Schema( +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]), } ) -CONDITION_SCHEMA = vol.Schema( +CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( { - vol.Required(CONF_CONDITION): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]), } ) -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( { - vol.Required(CONF_PLATFORM): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), } ) -def _is_domain(entity, domain): - return split_entity_id(entity.entity_id)[0] == domain - - -async def async_call_action_from_config(hass, config, variables, context, domain): +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, + domain: str, +): """Change state based on configuration.""" config = ACTION_SCHEMA(config) action_type = config[CONF_TYPE] @@ -119,7 +109,9 @@ async def async_call_action_from_config(hass, config, variables, context, domain ) -def async_condition_from_config(config, config_validation): +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" condition_type = config[CONF_TYPE] if condition_type == CONF_IS_ON: @@ -135,7 +127,12 @@ def async_condition_from_config(config, config_validation): return condition.state_from_config(state_config, config_validation) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_type = config[CONF_TYPE] if trigger_type == CONF_TURNED_ON: @@ -150,37 +147,56 @@ async def async_attach_trigger(hass, config, action, automation_info): state.CONF_TO: to_state, } - return await state.async_trigger(hass, state_config, action, automation_info) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) -async def _async_get_automations(hass, device_id, automation_templates, domain): +async def _async_get_automations( + hass: HomeAssistant, device_id: str, automation_templates: List[dict], domain: str +) -> List[dict]: """List device automations.""" automations = [] entity_registry = await hass.helpers.entity_registry.async_get_registry() - entities = async_entries_for_device(entity_registry, device_id) - domain_entities = [x for x in entities if _is_domain(x, domain)] - for entity in domain_entities: - for automation in automation_templates: - automation = dict(automation) - automation.update( - device_id=device_id, entity_id=entity.entity_id, domain=domain + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == domain + ] + + for entry in entries: + automations.extend( + ( + { + **template, + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": domain, + } + for template in automation_templates ) - automations.append(automation) + ) return automations -async def async_get_actions(hass, device_id, domain): +async def async_get_actions( + hass: HomeAssistant, device_id: str, domain: str +) -> List[dict]: """List device actions.""" return await _async_get_automations(hass, device_id, ENTITY_ACTIONS, domain) -async def async_get_conditions(hass, device_id, domain): +async def async_get_conditions( + hass: HomeAssistant, device_id: str, domain: str +) -> List[dict]: """List device conditions.""" return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain) -async def async_get_triggers(hass, device_id, domain): +async def async_get_triggers( + hass: HomeAssistant, device_id: str, domain: str +) -> List[dict]: """List device triggers.""" return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py new file mode 100644 index 00000000000000..ea37b8e9470534 --- /dev/null +++ b/homeassistant/components/light/device_action.py @@ -0,0 +1,30 @@ +"""Provides device actions for lights.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, Context +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import TemplateVarsType, ConfigType +from . import DOMAIN + + +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, +) -> None: + """Change state based on configuration.""" + config = ACTION_SCHEMA(config) + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN + ) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_automation.py b/homeassistant/components/light/device_automation.py deleted file mode 100644 index 61292d47449adf..00000000000000 --- a/homeassistant/components/light/device_automation.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Provides device automations for lights.""" -import voluptuous as vol - -from homeassistant.components.device_automation import toggle_entity -from homeassistant.const import CONF_DOMAIN -from . import DOMAIN - - -# mypy: allow-untyped-defs, no-check-untyped-defs - -ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) - -CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - -TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - - -async def async_call_action_from_config(hass, config, variables, context): - """Change state based on configuration.""" - config = ACTION_SCHEMA(config) - await toggle_entity.async_call_action_from_config( - hass, config, variables, context, DOMAIN - ) - - -def async_condition_from_config(config, config_validation): - """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) - - -async def async_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) - - -async def async_get_actions(hass, device_id): - """List device actions.""" - return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) - - -async def async_get_conditions(hass, device_id): - """List device conditions.""" - return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) - - -async def async_get_triggers(hass, device_id): - """List device triggers.""" - return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py new file mode 100644 index 00000000000000..a69ca7ab8f2f2f --- /dev/null +++ b/homeassistant/components/light/device_condition.py @@ -0,0 +1,28 @@ +"""Provides device conditions for lights.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.condition import ConditionCheckerType +from . import DOMAIN + + +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> ConditionCheckerType: + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + return toggle_entity.async_condition_from_config(config, config_validation) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py new file mode 100644 index 00000000000000..f2a82afdc2d41c --- /dev/null +++ b/homeassistant/components/light/device_trigger.py @@ -0,0 +1,33 @@ +"""Provides device trigger for lights.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN + + +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + return await toggle_entity.async_attach_trigger( + hass, config, action, automation_info + ) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers.""" + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py new file mode 100644 index 00000000000000..ca91cc70512be5 --- /dev/null +++ b/homeassistant/components/switch/device_action.py @@ -0,0 +1,30 @@ +"""Provides device actions for switches.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, Context +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import TemplateVarsType, ConfigType +from . import DOMAIN + + +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, +) -> None: + """Change state based on configuration.""" + config = ACTION_SCHEMA(config) + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN + ) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_automation.py b/homeassistant/components/switch/device_automation.py deleted file mode 100644 index 61292d47449adf..00000000000000 --- a/homeassistant/components/switch/device_automation.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Provides device automations for lights.""" -import voluptuous as vol - -from homeassistant.components.device_automation import toggle_entity -from homeassistant.const import CONF_DOMAIN -from . import DOMAIN - - -# mypy: allow-untyped-defs, no-check-untyped-defs - -ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) - -CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - -TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - - -async def async_call_action_from_config(hass, config, variables, context): - """Change state based on configuration.""" - config = ACTION_SCHEMA(config) - await toggle_entity.async_call_action_from_config( - hass, config, variables, context, DOMAIN - ) - - -def async_condition_from_config(config, config_validation): - """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) - - -async def async_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) - - -async def async_get_actions(hass, device_id): - """List device actions.""" - return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) - - -async def async_get_conditions(hass, device_id): - """List device conditions.""" - return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) - - -async def async_get_triggers(hass, device_id): - """List device triggers.""" - return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py new file mode 100644 index 00000000000000..032c765bf5907c --- /dev/null +++ b/homeassistant/components/switch/device_condition.py @@ -0,0 +1,28 @@ +"""Provides device conditions for switches.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.condition import ConditionCheckerType +from . import DOMAIN + + +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> ConditionCheckerType: + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + return toggle_entity.async_condition_from_config(config, config_validation) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py new file mode 100644 index 00000000000000..9be294d5460c99 --- /dev/null +++ b/homeassistant/components/switch/device_trigger.py @@ -0,0 +1,33 @@ +"""Provides device triggers for switches.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN + + +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + return await toggle_entity.async_attach_trigger( + hass, config, action, automation_info + ) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers.""" + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/zha/device_automation.py b/homeassistant/components/zha/device_trigger.py similarity index 84% rename from homeassistant/components/zha/device_automation.py rename to homeassistant/components/zha/device_trigger.py index 6a96ce5aa3e405..46e3beafcaedfd 100644 --- a/homeassistant/components/zha/device_automation.py +++ b/homeassistant/components/zha/device_trigger.py @@ -6,6 +6,7 @@ InvalidDeviceAutomationConfig, ) from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from . import DOMAIN from .core.const import DATA_ZHA, DATA_ZHA_GATEWAY @@ -16,20 +17,12 @@ DEVICE_IEEE = "device_ieee" ZHA_EVENT = "zha_event" -TRIGGER_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_PLATFORM): DEVICE, - vol.Required(CONF_TYPE): str, - vol.Required(CONF_SUBTYPE): str, - } - ) +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" config = TRIGGER_SCHEMA(config) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) @@ -48,7 +41,9 @@ async def async_trigger(hass, config, action, automation_info): event.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger}, } - return await event.async_trigger(hass, state_config, action, automation_info) + return await event.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) async def async_get_triggers(hass, device_id): diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 133251e779d1db..afb8c3934a7442 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -8,16 +8,14 @@ from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, TemplateVarsType - +from homeassistant.loader import async_get_integration from homeassistant.core import HomeAssistant, State from homeassistant.components import zone as zone_cmp -from homeassistant.components.device_automation import ( # noqa: F401 pylint: disable=unused-import - async_device_condition_from_config as async_device_from_config, -) from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, + CONF_DOMAIN, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, CONF_CONDITION, @@ -45,10 +43,12 @@ _LOGGER = logging.getLogger(__name__) +ConditionCheckerType = Callable[[HomeAssistant, TemplateVarsType], bool] + async def async_from_config( hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Turn a condition configuration into a method. Should be run on the event loop. @@ -74,13 +74,15 @@ async def async_from_config( check_factory = check_factory.func if asyncio.iscoroutinefunction(check_factory): - return cast(Callable[..., bool], await factory(hass, config, config_validation)) - return cast(Callable[..., bool], factory(config, config_validation)) + return cast( + ConditionCheckerType, await factory(hass, config, config_validation) + ) + return cast(ConditionCheckerType, factory(config, config_validation)) async def async_and_from_config( hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Create multi condition matcher using 'AND'.""" if config_validation: config = cv.AND_CONDITION_SCHEMA(config) @@ -107,7 +109,7 @@ def if_and_condition( async def async_or_from_config( hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Create multi condition matcher using 'OR'.""" if config_validation: config = cv.OR_CONDITION_SCHEMA(config) @@ -205,7 +207,7 @@ def async_numeric_state( def async_numeric_state_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with state based condition.""" if config_validation: config = cv.NUMERIC_STATE_CONDITION_SCHEMA(config) @@ -255,7 +257,7 @@ def state( def state_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with state based condition.""" if config_validation: config = cv.STATE_CONDITION_SCHEMA(config) @@ -327,7 +329,7 @@ def sun( def sun_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with sun based condition.""" if config_validation: config = cv.SUN_CONDITION_SCHEMA(config) @@ -370,7 +372,7 @@ def async_template( def async_template_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with state based condition.""" if config_validation: config = cv.TEMPLATE_CONDITION_SCHEMA(config) @@ -427,7 +429,7 @@ def time( def time_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with time based condition.""" if config_validation: config = cv.TIME_CONDITION_SCHEMA(config) @@ -476,7 +478,7 @@ def zone( def zone_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with zone based condition.""" if config_validation: config = cv.ZONE_CONDITION_SCHEMA(config) @@ -488,3 +490,17 @@ def if_in_zone(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: return zone(hass, zone_entity_id, entity_id) return if_in_zone + + +async def async_device_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True +) -> ConditionCheckerType: + """Test a device condition.""" + if config_validation: + config = cv.DEVICE_CONDITION_SCHEMA(config) + integration = await async_get_integration(hass, config[CONF_DOMAIN]) + platform = integration.get_platform("device_condition") + return cast( + ConditionCheckerType, + platform.async_condition_from_config(config, config_validation), # type: ignore + ) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 952fa41c42c1ce..113f2437ce8cea 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -827,11 +827,16 @@ def validator(value): } ) -DEVICE_CONDITION_SCHEMA = vol.Schema( - {vol.Required(CONF_CONDITION): "device", vol.Required(CONF_DOMAIN): str}, - extra=vol.ALLOW_EXTRA, +DEVICE_CONDITION_BASE_SCHEMA = vol.Schema( + { + vol.Required(CONF_CONDITION): "device", + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): str, + } ) +DEVICE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + CONDITION_SCHEMA: vol.Schema = vol.Any( NUMERIC_STATE_CONDITION_SCHEMA, STATE_CONDITION_SCHEMA, @@ -862,11 +867,12 @@ def validator(value): } ) -DEVICE_ACTION_SCHEMA = vol.Schema( - {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str}, - extra=vol.ALLOW_EXTRA, +DEVICE_ACTION_BASE_SCHEMA = vol.Schema( + {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str} ) +DEVICE_ACTION_SCHEMA = DEVICE_ACTION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + SCRIPT_SCHEMA = vol.All( ensure_list, [ diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 23728b651098aa..14ff873d4d173b 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -336,7 +336,7 @@ async def _async_device_automation(self, action, variables, context): self.last_action = action.get(CONF_ALIAS, "device automation") self._log("Executing step %s" % self.last_action) integration = await async_get_integration(self.hass, action[CONF_DOMAIN]) - platform = integration.get_platform("device_automation") + platform = integration.get_platform("device_action") await platform.async_call_action_from_config( self.hass, action, variables, context ) diff --git a/tests/common.py b/tests/common.py index fda5c743222703..bc39b1f5e0b62a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -54,7 +54,9 @@ from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe - +from homeassistant.components.device_automation import ( # noqa + _async_get_device_automations as async_get_device_automations, +) _TEST_INSTANCE_PORT = SERVER_PORT _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/binary_sensor/test_device_automation.py b/tests/components/binary_sensor/test_device_automation.py deleted file mode 100644 index 91124d47f4e4ef..00000000000000 --- a/tests/components/binary_sensor/test_device_automation.py +++ /dev/null @@ -1,309 +0,0 @@ -"""The test for binary_sensor device automation.""" -import pytest - -from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES -from homeassistant.components.binary_sensor.device_automation import ( - ENTITY_CONDITIONS, - ENTITY_TRIGGERS, -) -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) -from homeassistant.helpers import device_registry - -from tests.common import ( - MockConfigEntry, - async_mock_service, - mock_device_registry, - mock_registry, -) - - -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -@pytest.fixture -def entity_reg(hass): - """Return an empty, loaded, registry.""" - return mock_registry(hass) - - -@pytest.fixture -def calls(hass): - """Track calls to a mock serivce.""" - return async_mock_service(hass, "test", "automation") - - -def _same_lists(a, b): - if len(a) != len(b): - return False - - for d in a: - if d not in b: - return False - return True - - -async def test_get_actions(hass, device_reg, entity_reg): - """Test we get the expected actions from a binary_sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create( - DOMAIN, - "test", - platform.ENTITIES["battery"].unique_id, - device_id=device_entry.id, - ) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_actions = [] - actions = await async_get_device_automations( - hass, "async_get_actions", device_entry.id - ) - assert _same_lists(actions, expected_actions) - - -async def test_get_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a binary_sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - for device_class in DEVICE_CLASSES: - entity_reg.async_get_or_create( - DOMAIN, - "test", - platform.ENTITIES[device_class].unique_id, - device_id=device_entry.id, - ) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_conditions = [ - { - "condition": "device", - "domain": DOMAIN, - "type": condition["type"], - "device_id": device_entry.id, - "entity_id": platform.ENTITIES[device_class].entity_id, - } - for device_class in DEVICE_CLASSES - for condition in ENTITY_CONDITIONS[device_class] - ] - conditions = await async_get_device_automations( - hass, "async_get_conditions", device_entry.id - ) - assert _same_lists(conditions, expected_conditions) - - -async def test_get_triggers(hass, device_reg, entity_reg): - """Test we get the expected triggers from a binary_sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - for device_class in DEVICE_CLASSES: - entity_reg.async_get_or_create( - DOMAIN, - "test", - platform.ENTITIES[device_class].unique_id, - device_id=device_entry.id, - ) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_triggers = [ - { - "platform": "device", - "domain": DOMAIN, - "type": trigger["type"], - "device_id": device_entry.id, - "entity_id": platform.ENTITIES[device_class].entity_id, - } - for device_class in DEVICE_CLASSES - for trigger in ENTITY_TRIGGERS[device_class] - ] - triggers = await async_get_device_automations( - hass, "async_get_triggers", device_entry.id - ) - assert _same_lists(triggers, expected_triggers) - - -async def test_if_fires_on_state_change(hass, calls): - """Test for on and off triggers firing.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - sensor1 = platform.ENTITIES["battery"] - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": sensor1.entity_id, - "type": "bat_low", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": sensor1.entity_id, - "type": "not_bat_low", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "not_bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(sensor1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.states.async_set(sensor1.entity_id, STATE_OFF) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "not_bat_low state - {} - on - off - None".format( - sensor1.entity_id - ) - - hass.states.async_set(sensor1.entity_id, STATE_ON) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "bat_low state - {} - off - on - None".format( - sensor1.entity_id - ) - - -async def test_if_state(hass, calls): - """Test for turn_on and turn_off conditions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - sensor1 = platform.ENTITIES["battery"] - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": sensor1.entity_id, - "type": "is_bat_low", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": sensor1.entity_id, - "type": "is_not_bat_low", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(sensor1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" - - hass.states.async_set(sensor1.entity_id, STATE_OFF) - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py new file mode 100644 index 00000000000000..b5502d8fe3da39 --- /dev/null +++ b/tests/components/binary_sensor/test_device_condition.py @@ -0,0 +1,144 @@ +"""The test for binary_sensor device automation.""" +import pytest + +from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": condition["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for condition in ENTITY_CONDITIONS[device_class] + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_bat_low", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_not_bat_low", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py new file mode 100644 index 00000000000000..5be354c78fc5cf --- /dev/null +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -0,0 +1,154 @@ +"""The test for binary_sensor device automation.""" +import pytest + +from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.binary_sensor.device_trigger import ENTITY_TRIGGERS +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": trigger["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for trigger in ENTITY_TRIGGERS[device_class] + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_if_fires_on_state_change(hass, calls): + """Test for on and off triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "bat_low", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "not_bat_low", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "not_bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "not_bat_low device - {} - on - off - None".format( + sensor1.entity_id + ) + + hass.states.async_set(sensor1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "bat_low device - {} - off - on - None".format( + sensor1.entity_id + ) diff --git a/tests/components/binary_sensor/test_binary_sensor.py b/tests/components/binary_sensor/test_init.py similarity index 100% rename from tests/components/binary_sensor/test_binary_sensor.py rename to tests/components/binary_sensor/test_init.py diff --git a/tests/components/deconz/test_device_automation.py b/tests/components/deconz/test_device_trigger.py similarity index 71% rename from tests/components/deconz/test_device_automation.py rename to tests/components/deconz/test_device_trigger.py index 0be566d4b52e43..6590028d76638a 100644 --- a/tests/components/deconz/test_device_automation.py +++ b/tests/components/deconz/test_device_trigger.py @@ -3,9 +3,9 @@ from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) +from homeassistant.components.deconz import device_trigger + +from tests.common import async_get_device_automations BRIDGEID = "0123456789" @@ -49,16 +49,6 @@ DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG, "sensors": DECONZ_SENSOR} -def _same_lists(a, b): - if len(a) != len(b): - return False - - for d in a: - if d not in b: - return False - return True - - async def setup_deconz(hass, options): """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( @@ -88,51 +78,51 @@ async def test_get_triggers(hass): """Test triggers work.""" gateway = await setup_deconz(hass, options={}) device_id = gateway.events[0].device_id - triggers = await async_get_device_automations(hass, "async_get_triggers", device_id) + triggers = await async_get_device_automations(hass, "trigger", device_id) expected_triggers = [ { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_SHORT_PRESS, - "subtype": deconz.device_automation.CONF_TURN_ON, + "type": device_trigger.CONF_SHORT_PRESS, + "subtype": device_trigger.CONF_TURN_ON, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_LONG_PRESS, - "subtype": deconz.device_automation.CONF_TURN_ON, + "type": device_trigger.CONF_LONG_PRESS, + "subtype": device_trigger.CONF_TURN_ON, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_LONG_RELEASE, - "subtype": deconz.device_automation.CONF_TURN_ON, + "type": device_trigger.CONF_LONG_RELEASE, + "subtype": device_trigger.CONF_TURN_ON, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_SHORT_PRESS, - "subtype": deconz.device_automation.CONF_TURN_OFF, + "type": device_trigger.CONF_SHORT_PRESS, + "subtype": device_trigger.CONF_TURN_OFF, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_LONG_PRESS, - "subtype": deconz.device_automation.CONF_TURN_OFF, + "type": device_trigger.CONF_LONG_PRESS, + "subtype": device_trigger.CONF_TURN_OFF, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_LONG_RELEASE, - "subtype": deconz.device_automation.CONF_TURN_OFF, + "type": device_trigger.CONF_LONG_RELEASE, + "subtype": device_trigger.CONF_TURN_OFF, }, ] - assert _same_lists(triggers, expected_triggers) + assert triggers == expected_triggers diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py new file mode 100644 index 00000000000000..bb50778db52f70 --- /dev/null +++ b/tests/components/light/test_device_action.py @@ -0,0 +1,140 @@ +"""The test for light device automation.""" +import pytest + +from homeassistant.components.light import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a light.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "toggle", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert actions == expected_actions + + +async def test_action(hass, calls): + """Test for turn_on and turn_off actions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_off", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_on", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "toggle", + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py deleted file mode 100644 index 27b8b860d7227c..00000000000000 --- a/tests/components/light/test_device_automation.py +++ /dev/null @@ -1,373 +0,0 @@ -"""The test for light device automation.""" -import pytest - -from homeassistant.components.light import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) -from homeassistant.helpers import device_registry - -from tests.common import ( - MockConfigEntry, - async_mock_service, - mock_device_registry, - mock_registry, -) - - -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -@pytest.fixture -def entity_reg(hass): - """Return an empty, loaded, registry.""" - return mock_registry(hass) - - -@pytest.fixture -def calls(hass): - """Track calls to a mock serivce.""" - return async_mock_service(hass, "test", "automation") - - -def _same_lists(a, b): - if len(a) != len(b): - return False - - for d in a: - if d not in b: - return False - return True - - -async def test_get_actions(hass, device_reg, entity_reg): - """Test we get the expected actions from a light.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_actions = [ - { - "domain": DOMAIN, - "type": "turn_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "domain": DOMAIN, - "type": "turn_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "domain": DOMAIN, - "type": "toggle", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - actions = await async_get_device_automations( - hass, "async_get_actions", device_entry.id - ) - assert _same_lists(actions, expected_actions) - - -async def test_get_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a light.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_conditions = [ - { - "condition": "device", - "domain": DOMAIN, - "type": "is_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - conditions = await async_get_device_automations( - hass, "async_get_conditions", device_entry.id - ) - assert _same_lists(conditions, expected_conditions) - - -async def test_get_triggers(hass, device_reg, entity_reg): - """Test we get the expected triggers from a light.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_triggers = [ - { - "platform": "device", - "domain": DOMAIN, - "type": "turned_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "turned_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - triggers = await async_get_device_automations( - hass, "async_get_triggers", device_entry.id - ) - assert _same_lists(triggers, expected_triggers) - - -async def test_if_fires_on_state_change(hass, calls): - """Test for turn_on and turn_off triggers firing.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turned_on", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turned_off", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.states.async_set(ent1.entity_id, STATE_OFF) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( - ent1.entity_id - ) - - hass.states.async_set(ent1.entity_id, STATE_ON) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( - ent1.entity_id - ) - - -async def test_if_state(hass, calls): - """Test for turn_on and turn_off conditions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "is_on", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "is_off", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" - - hass.states.async_set(ent1.entity_id, STATE_OFF) - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" - - -async def test_action(hass, calls): - """Test for turn_on and turn_off actions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turn_off", - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turn_on", - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event3"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "toggle", - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event1") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - - hass.bus.async_fire("test_event3") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event3") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py new file mode 100644 index 00000000000000..8009fbd633798a --- /dev/null +++ b/tests/components/light/test_device_condition.py @@ -0,0 +1,136 @@ +"""The test for light device automation.""" +import pytest + +from homeassistant.components.light import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a light.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py new file mode 100644 index 00000000000000..9b540c7aa15af7 --- /dev/null +++ b/tests/components/light/test_device_trigger.py @@ -0,0 +1,147 @@ +"""The test for light device automation.""" +import pytest + +from homeassistant.components.light import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a light.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "turn_off device - {} - on - off - None".format( + ent1.entity_id + ) + + hass.states.async_set(ent1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( + ent1.entity_id + ) diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py new file mode 100644 index 00000000000000..888e06e0214db8 --- /dev/null +++ b/tests/components/switch/test_device_action.py @@ -0,0 +1,142 @@ +"""The test for switch device automation.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "toggle", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert actions == expected_actions + + +async def test_action(hass, calls): + """Test for turn_on and turn_off actions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_off", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_on", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "toggle", + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/switch/test_device_automation.py b/tests/components/switch/test_device_automation.py deleted file mode 100644 index 1ebe4785761aa5..00000000000000 --- a/tests/components/switch/test_device_automation.py +++ /dev/null @@ -1,373 +0,0 @@ -"""The test for switch device automation.""" -import pytest - -from homeassistant.components.switch import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) -from homeassistant.helpers import device_registry - -from tests.common import ( - MockConfigEntry, - async_mock_service, - mock_device_registry, - mock_registry, -) - - -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -@pytest.fixture -def entity_reg(hass): - """Return an empty, loaded, registry.""" - return mock_registry(hass) - - -@pytest.fixture -def calls(hass): - """Track calls to a mock serivce.""" - return async_mock_service(hass, "test", "automation") - - -def _same_lists(a, b): - if len(a) != len(b): - return False - - for d in a: - if d not in b: - return False - return True - - -async def test_get_actions(hass, device_reg, entity_reg): - """Test we get the expected actions from a switch.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_actions = [ - { - "domain": DOMAIN, - "type": "turn_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "domain": DOMAIN, - "type": "turn_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "domain": DOMAIN, - "type": "toggle", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - actions = await async_get_device_automations( - hass, "async_get_actions", device_entry.id - ) - assert _same_lists(actions, expected_actions) - - -async def test_get_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a switch.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_conditions = [ - { - "condition": "device", - "domain": DOMAIN, - "type": "is_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - conditions = await async_get_device_automations( - hass, "async_get_conditions", device_entry.id - ) - assert _same_lists(conditions, expected_conditions) - - -async def test_get_triggers(hass, device_reg, entity_reg): - """Test we get the expected triggers from a switch.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_triggers = [ - { - "platform": "device", - "domain": DOMAIN, - "type": "turned_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "turned_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - triggers = await async_get_device_automations( - hass, "async_get_triggers", device_entry.id - ) - assert _same_lists(triggers, expected_triggers) - - -async def test_if_fires_on_state_change(hass, calls): - """Test for turn_on and turn_off triggers firing.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turned_on", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turned_off", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.states.async_set(ent1.entity_id, STATE_OFF) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( - ent1.entity_id - ) - - hass.states.async_set(ent1.entity_id, STATE_ON) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( - ent1.entity_id - ) - - -async def test_if_state(hass, calls): - """Test for turn_on and turn_off conditions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "is_on", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "is_off", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" - - hass.states.async_set(ent1.entity_id, STATE_OFF) - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" - - -async def test_action(hass, calls): - """Test for turn_on and turn_off actions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turn_off", - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turn_on", - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event3"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "toggle", - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event1") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - - hass.bus.async_fire("test_event3") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event3") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py new file mode 100644 index 00000000000000..e2ce5a373d227f --- /dev/null +++ b/tests/components/switch/test_device_condition.py @@ -0,0 +1,138 @@ +"""The test for switch device automation.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py new file mode 100644 index 00000000000000..43af9fe3df34b2 --- /dev/null +++ b/tests/components/switch/test_device_trigger.py @@ -0,0 +1,147 @@ +"""The test for switch device automation.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "turn_off device - {} - on - off - None".format( + ent1.entity_id + ) + + hass.states.async_set(ent1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( + ent1.entity_id + ) diff --git a/tests/components/zha/test_device_automation.py b/tests/components/zha/test_device_automation.py index 9de04ae8e662e8..5a4b9d5616e8fe 100644 --- a/tests/components/zha/test_device_automation.py +++ b/tests/components/zha/test_device_automation.py @@ -4,9 +4,6 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) from homeassistant.components.switch import DOMAIN from homeassistant.components.zha.core.const import CHANNEL_ON_OFF from homeassistant.helpers.device_registry import async_get_registry @@ -14,7 +11,7 @@ from .common import async_enable_traffic, async_init_zigpy_device -from tests.common import async_mock_service +from tests.common import async_mock_service, async_get_device_automations ON = 1 OFF = 0 @@ -73,9 +70,7 @@ async def test_triggers(hass, config_entry, zha_gateway): ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) - triggers = await async_get_device_automations( - hass, "async_get_triggers", reg_device.id - ) + triggers = await async_get_device_automations(hass, "trigger", reg_device.id) expected_triggers = [ { @@ -136,9 +131,7 @@ async def test_no_triggers(hass, config_entry, zha_gateway): ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) - triggers = await async_get_device_automations( - hass, "async_get_triggers", reg_device.id - ) + triggers = await async_get_device_automations(hass, "trigger", reg_device.id) assert triggers == [] From 9c9c921922876949fae384a1aea937ef55114168 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 Sep 2019 00:38:20 +0200 Subject: [PATCH 147/296] Use Python3 new super syntax sugar (#26890) --- homeassistant/components/bloomsky/camera.py | 2 +- homeassistant/components/foscam/camera.py | 2 +- homeassistant/components/graphite/__init__.py | 2 +- homeassistant/components/loopenergy/sensor.py | 4 ++-- homeassistant/components/mochad/__init__.py | 2 +- homeassistant/components/nest/binary_sensor.py | 2 +- homeassistant/components/nest/camera.py | 2 +- homeassistant/components/netatmo/camera.py | 2 +- homeassistant/components/nuimo_controller/__init__.py | 2 +- homeassistant/components/nx584/binary_sensor.py | 2 +- homeassistant/components/onkyo/media_player.py | 2 +- homeassistant/components/ring/binary_sensor.py | 2 +- homeassistant/components/ring/camera.py | 2 +- homeassistant/components/ring/sensor.py | 2 +- homeassistant/components/squeezebox/media_player.py | 2 +- homeassistant/components/tcp/sensor.py | 2 +- homeassistant/components/tplink/device_tracker.py | 4 ++-- homeassistant/components/ubus/device_tracker.py | 2 +- homeassistant/components/uvc/camera.py | 2 +- homeassistant/components/wink/__init__.py | 4 ++-- homeassistant/components/wink/switch.py | 2 +- homeassistant/components/zigbee/__init__.py | 4 ++-- homeassistant/components/zwave/binary_sensor.py | 2 +- homeassistant/helpers/logging.py | 2 +- homeassistant/util/yaml/loader.py | 2 +- tests/components/smhi/common.py | 2 +- 26 files changed, 30 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/bloomsky/camera.py b/homeassistant/components/bloomsky/camera.py index 9b8c4ab283f3b2..d62dede5abdab4 100644 --- a/homeassistant/components/bloomsky/camera.py +++ b/homeassistant/components/bloomsky/camera.py @@ -19,7 +19,7 @@ class BloomSkyCamera(Camera): def __init__(self, bs, device): """Initialize access to the BloomSky camera images.""" - super(BloomSkyCamera, self).__init__() + super().__init__() self._name = device["DeviceName"] self._id = device["DeviceID"] self._bloomsky = bs diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 37f792cec45b83..63e9956d0dfaf6 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -41,7 +41,7 @@ def __init__(self, device_info): """Initialize a Foscam camera.""" from libpyfoscam import FoscamCamera - super(FoscamCam, self).__init__() + super().__init__() ip_address = device_info.get(CONF_IP) port = device_info.get(CONF_PORT) diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 550a0ce1d1303e..3809249bea698f 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -64,7 +64,7 @@ class GraphiteFeeder(threading.Thread): def __init__(self, hass, host, port, prefix): """Initialize the feeder.""" - super(GraphiteFeeder, self).__init__(daemon=True) + super().__init__(daemon=True) self._hass = hass self._host = host self._port = port diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py index 56a87ce4345867..994c3e2fd8952a 100644 --- a/homeassistant/components/loopenergy/sensor.py +++ b/homeassistant/components/loopenergy/sensor.py @@ -122,7 +122,7 @@ class LoopEnergyElec(LoopEnergyDevice): def __init__(self, controller): """Initialize the sensor.""" - super(LoopEnergyElec, self).__init__(controller) + super().__init__(controller) self._name = "Power Usage" self._controller.subscribe_elecricity(self._callback) @@ -136,7 +136,7 @@ class LoopEnergyGas(LoopEnergyDevice): def __init__(self, controller): """Initialize the sensor.""" - super(LoopEnergyGas, self).__init__(controller) + super().__init__(controller) self._name = "Gas Usage" self._controller.subscribe_gas(self._callback) diff --git a/homeassistant/components/mochad/__init__.py b/homeassistant/components/mochad/__init__.py index 07028de0d42419..77426e8ae2c911 100644 --- a/homeassistant/components/mochad/__init__.py +++ b/homeassistant/components/mochad/__init__.py @@ -64,7 +64,7 @@ class MochadCtrl: def __init__(self, host, port): """Initialize a PyMochad controller.""" - super(MochadCtrl, self).__init__() + super().__init__() self._host = host self._port = port diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index 0f3ae7da710edf..05170a54ed1b7a 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -141,7 +141,7 @@ class NestActivityZoneSensor(NestBinarySensor): def __init__(self, structure, device, zone): """Initialize the sensor.""" - super(NestActivityZoneSensor, self).__init__(structure, device, "") + super().__init__(structure, device, "") self.zone = zone self._name = f"{self._name} {self.zone.name} activity" diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index 37fd18efb6d5e9..efc0bfbc8227e1 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -34,7 +34,7 @@ class NestCamera(Camera): def __init__(self, structure, device): """Initialize a Nest Camera.""" - super(NestCamera, self).__init__() + super().__init__() self.structure = structure self.device = device self._location = None diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index d18ff9fc46c800..60428961cb95be 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -69,7 +69,7 @@ class NetatmoCamera(Camera): def __init__(self, data, camera_name, home, camera_type, verify_ssl, quality): """Set up for access to the Netatmo camera images.""" - super(NetatmoCamera, self).__init__() + super().__init__() self._data = data self._camera_name = camera_name self._verify_ssl = verify_ssl diff --git a/homeassistant/components/nuimo_controller/__init__.py b/homeassistant/components/nuimo_controller/__init__.py index 42d35b60b250b3..8fa3897b735776 100644 --- a/homeassistant/components/nuimo_controller/__init__.py +++ b/homeassistant/components/nuimo_controller/__init__.py @@ -76,7 +76,7 @@ class NuimoThread(threading.Thread): def __init__(self, hass, mac, name): """Initialize thread object.""" - super(NuimoThread, self).__init__() + super().__init__() self._hass = hass self._mac = mac self._name = name diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 8b26a958a6ffa7..6f1c66d8d879b6 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -107,7 +107,7 @@ class NX584Watcher(threading.Thread): def __init__(self, client, zone_sensors): """Initialize NX584 watcher thread.""" - super(NX584Watcher, self).__init__() + super().__init__() self.daemon = True self._client = client self._zone_sensors = zone_sensors diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 9ec8c56d770a00..af92f6c5f0510a 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -373,7 +373,7 @@ def __init__(self, zone, receiver, sources, name=None): """Initialize the Zone with the zone identifier.""" self._zone = zone self._supports_volume = True - super(OnkyoDeviceZone, self).__init__(receiver, sources, name) + super().__init__(receiver, sources, name) def update(self): """Get the latest state from the device.""" diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 6806df0408fb78..86d26ec25b4ea6 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -65,7 +65,7 @@ class RingBinarySensor(BinarySensorDevice): def __init__(self, hass, data, sensor_type): """Initialize a sensor for Ring device.""" - super(RingBinarySensor, self).__init__() + super().__init__() self._sensor_type = sensor_type self._data = data self._name = "{0} {1}".format( diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index ef86ea6734cf6a..461b3a199d7721 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -75,7 +75,7 @@ class RingCam(Camera): def __init__(self, hass, camera, device_info): """Initialize a Ring Door Bell camera.""" - super(RingCam, self).__init__() + super().__init__() self._camera = camera self._hass = hass self._name = self._camera.name diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index af661f4571c210..6a64226a053e97 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -110,7 +110,7 @@ class RingSensor(Entity): def __init__(self, hass, data, sensor_type): """Initialize a sensor for Ring device.""" - super(RingSensor, self).__init__() + super().__init__() self._sensor_type = sensor_type self._data = data self._extra = None diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 9e62e7ee0dbc83..6540fca1405871 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -246,7 +246,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): def __init__(self, lms, player_id, name): """Initialize the SqueezeBox device.""" - super(SqueezeBoxDevice, self).__init__() + super().__init__() self._lms = lms self._id = player_id self._status = {} diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index 70301f203f8ccb..a387b3fc0bb7f7 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -81,7 +81,7 @@ def name(self): name = self._config[CONF_NAME] if name is not None: return name - return super(TcpSensor, self).name + return super().name @property def state(self): diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py index 60d495738338e7..e7f87074cb4c00 100644 --- a/homeassistant/components/tplink/device_tracker.py +++ b/homeassistant/components/tplink/device_tracker.py @@ -245,7 +245,7 @@ def __init__(self, config): """Initialize the scanner.""" self.stok = "" self.sysauth = "" - super(Tplink3DeviceScanner, self).__init__(config) + super().__init__(config) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -365,7 +365,7 @@ def __init__(self, config): """Initialize the scanner.""" self.credentials = "" self.token = "" - super(Tplink4DeviceScanner, self).__init__(config) + super().__init__(config) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index f14ea5af02cd43..8c83de202a468c 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -146,7 +146,7 @@ class DnsmasqUbusDeviceScanner(UbusDeviceScanner): def __init__(self, config): """Initialize the scanner.""" - super(DnsmasqUbusDeviceScanner, self).__init__(config) + super().__init__(config) self.leasefile = None def _generate_mac2name(self): diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 01a3338e540760..20aae3849ab354 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -78,7 +78,7 @@ class UnifiVideoCamera(Camera): def __init__(self, nvr, uuid, name, password): """Initialize an Unifi camera.""" - super(UnifiVideoCamera, self).__init__() + super().__init__() self._nvr = nvr self._uuid = uuid self._name = name diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 5af784359d83db..d0bb27c06e11b9 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -863,7 +863,7 @@ def icon(self): @property def device_state_attributes(self): """Return the device state attributes.""" - attributes = super(WinkSirenDevice, self).device_state_attributes + attributes = super().device_state_attributes auto_shutoff = self.wink.auto_shutoff() if auto_shutoff is not None: @@ -921,7 +921,7 @@ def name(self): @property def device_state_attributes(self): """Return the device state attributes.""" - attributes = super(WinkNimbusDialDevice, self).device_state_attributes + attributes = super().device_state_attributes dial_attributes = self.dial_attributes() return {**attributes, **dial_attributes} diff --git a/homeassistant/components/wink/switch.py b/homeassistant/components/wink/switch.py index d888fb82528f67..07d3ff4becc04c 100644 --- a/homeassistant/components/wink/switch.py +++ b/homeassistant/components/wink/switch.py @@ -53,7 +53,7 @@ def turn_off(self, **kwargs): @property def device_state_attributes(self): """Return the state attributes.""" - attributes = super(WinkToggleDevice, self).device_state_attributes + attributes = super().device_state_attributes try: event = self.wink.last_event() if event is not None: diff --git a/homeassistant/components/zigbee/__init__.py b/homeassistant/components/zigbee/__init__.py index 7c1979f3ab9542..31cbc0c65b6aa7 100644 --- a/homeassistant/components/zigbee/__init__.py +++ b/homeassistant/components/zigbee/__init__.py @@ -172,7 +172,7 @@ class ZigBeeDigitalInConfig(ZigBeePinConfig): def __init__(self, config): """Initialise the Zigbee Digital input config.""" - super(ZigBeeDigitalInConfig, self).__init__(config) + super().__init__(config) self._bool2state, self._state2bool = self.boolean_maps @property @@ -216,7 +216,7 @@ class ZigBeeDigitalOutConfig(ZigBeePinConfig): def __init__(self, config): """Initialize the Zigbee Digital out.""" - super(ZigBeeDigitalOutConfig, self).__init__(config) + super().__init__(config) self._bool2state, self._state2bool = self.boolean_maps self._should_poll = config.get("poll", False) diff --git a/homeassistant/components/zwave/binary_sensor.py b/homeassistant/components/zwave/binary_sensor.py index fd18772edbf5ef..a28f53f93d463a 100644 --- a/homeassistant/components/zwave/binary_sensor.py +++ b/homeassistant/components/zwave/binary_sensor.py @@ -71,7 +71,7 @@ class ZWaveTriggerSensor(ZWaveBinarySensor): def __init__(self, values, device_class): """Initialize the sensor.""" - super(ZWaveTriggerSensor, self).__init__(values, device_class) + super().__init__(values, device_class) # Set default off delay to 60 sec self.re_arm_sec = 60 self.invalidate_after = None diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py index e69564453fada0..dd9e383380191c 100644 --- a/homeassistant/helpers/logging.py +++ b/homeassistant/helpers/logging.py @@ -29,7 +29,7 @@ class KeywordStyleAdapter(logging.LoggerAdapter): def __init__(self, logger, extra=None): """Initialize a new StyleAdapter for the provided logger.""" - super(KeywordStyleAdapter, self).__init__(logger, extra or {}) + super().__init__(logger, extra or {}) def log(self, level, msg, *args, **kwargs): """Log the message provided at the appropriate level.""" diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index ccc55691ee1b74..9822b7c63c2124 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -48,7 +48,7 @@ class SafeLineLoader(yaml.SafeLoader): def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node: """Annotate a node with the first line it was seen.""" last_line: int = self.line - node: yaml.nodes.Node = super(SafeLineLoader, self).compose_node(parent, index) + node: yaml.nodes.Node = super().compose_node(parent, index) node.__line__ = last_line + 1 # type: ignore return node diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py index ecf904ac9c9f41..74c8a99b51fcf7 100644 --- a/tests/components/smhi/common.py +++ b/tests/components/smhi/common.py @@ -8,4 +8,4 @@ class AsyncMock(Mock): # pylint: disable=W0235 async def __call__(self, *args, **kwargs): """Hack for async support for Mock.""" - return super(AsyncMock, self).__call__(*args, **kwargs) + return super().__call__(*args, **kwargs) From 24c8db0121fee8dd5f2a0ad9e04bfa2f116b5d1d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 25 Sep 2019 00:32:16 +0000 Subject: [PATCH 148/296] [ci skip] Translation update --- .../binary_sensor/.translations/sl.json | 92 ++++++++++++++ .../components/izone/.translations/sl.json | 15 +++ .../moon/.translations/sensor.no.json | 2 +- .../components/plex/.translations/ko.json | 2 +- .../components/plex/.translations/ru.json | 2 +- .../components/plex/.translations/sl.json | 45 +++++++ .../components/zha/.translations/en.json | 116 +++++++++--------- .../components/zha/.translations/no.json | 21 ++++ 8 files changed, 234 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/binary_sensor/.translations/sl.json create mode 100644 homeassistant/components/izone/.translations/sl.json create mode 100644 homeassistant/components/plex/.translations/sl.json diff --git a/homeassistant/components/binary_sensor/.translations/sl.json b/homeassistant/components/binary_sensor/.translations/sl.json new file mode 100644 index 00000000000000..6b4e144d9a6ac2 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/sl.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} ima prazno baterijo", + "is_cold": "{entity_name} je hladen", + "is_connected": "{entity_name} je povezan", + "is_gas": "{entity_name} zaznava plin", + "is_hot": "{entity_name} je vro\u010d", + "is_light": "{entity_name} zaznava svetlobo", + "is_locked": "{entity_name} je zaklenjen", + "is_moist": "{entity_name} je vla\u017een", + "is_motion": "{entity_name} zaznava gibanje", + "is_moving": "{entity_name} se premika", + "is_no_gas": "{entity_name} ne zaznava plina", + "is_no_light": "{entity_name} ne zaznava svetlobe", + "is_no_motion": "{entity_name} ne zaznava gibanja", + "is_no_problem": "{entity_name} ne zaznava te\u017eav", + "is_no_smoke": "{entity_name} ne zaznava dima", + "is_no_sound": "{entity_name} ne zaznava zvoka", + "is_no_vibration": "{entity_name} ne zazna vibracij", + "is_not_bat_low": "{entity_name} baterija je polna", + "is_not_cold": "{entity_name} ni hladen", + "is_not_connected": "{entity_name} ni povezan", + "is_not_hot": "{entity_name} ni vro\u010d", + "is_not_locked": "{entity_name} je odklenjen", + "is_not_moist": "{entity_name} je suh", + "is_not_moving": "{entity_name} se ne premika", + "is_not_occupied": "{entity_name} ni zaseden", + "is_not_open": "{entity_name} je zaprt", + "is_not_plugged_in": "{entity_name} je odklopljen", + "is_not_powered": "{entity_name} ni napajan", + "is_not_present": "{entity_name} ni prisoten", + "is_not_unsafe": "{entity_name} je varen", + "is_occupied": "{entity_name} je zaseden", + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen", + "is_open": "{entity_name} je odprt", + "is_plugged_in": "{entity_name} je priklju\u010den", + "is_powered": "{entity_name} je vklopljen", + "is_present": "{entity_name} je prisoten", + "is_problem": "{entity_name} zaznava te\u017eavo", + "is_smoke": "{entity_name} zaznava dim", + "is_sound": "{entity_name} zaznava zvok", + "is_unsafe": "{entity_name} ni varen", + "is_vibration": "{entity_name} zaznava vibracije" + }, + "trigger_type": { + "bat_low": "{entity_name} ima prazno baterijo", + "closed": "{entity_name} zaprto", + "cold": "{entity_name} je postal hladen", + "connected": "{entity_name} povezan", + "gas": "{entity_name} za\u010del zaznavati plin", + "hot": "{entity_name} je postal vro\u010d", + "light": "{entity_name} za\u010del zaznavati svetlobo", + "locked": "{entity_name} zaklenjen", + "moist\u00a7": "{entity_name} postal vla\u017een", + "motion": "{entity_name} za\u010del zaznavati gibanje", + "moving": "{entity_name} se je za\u010del premikati", + "no_gas": "{entity_name} prenehal zaznavati plin", + "no_light": "{entity_name} prenehal zaznavati svetlobo", + "no_motion": "{entity_name} prenehal zaznavati gibanje", + "no_problem": "{entity_name} prenehal odkrivati te\u017eavo", + "no_smoke": "{entity_name} prenehal zaznavati dim", + "no_sound": "{entity_name} prenehal zaznavati zvok", + "no_vibration": "{entity_name} prenehal zaznavati vibracije", + "not_bat_low": "{entity_name} ima polno baterijo", + "not_cold": "{entity_name} ni ve\u010d hladen", + "not_connected": "{entity_name} prekinjen", + "not_hot": "{entity_name} ni ve\u010d vro\u010d", + "not_locked": "{entity_name} odklenjen", + "not_moist": "{entity_name} je postalo suh", + "not_moving": "{entity_name} se je prenehal premikati", + "not_occupied": "{entity_name} ni zaseden", + "not_plugged_in": "{entity_name} odklopljen", + "not_powered": "{entity_name} ni napajan", + "not_present": "{entity_name} ni prisoten", + "not_unsafe": "{entity_name} je postal varen", + "occupied": "{entity_name} postal zaseden", + "opened": "{entity_name} odprl", + "plugged_in": "{entity_name} priklju\u010den", + "powered": "{entity_name} priklopljen", + "present": "{entity_name} prisoten", + "problem": "{entity_name} za\u010del odkrivati te\u017eavo", + "smoke": "{entity_name} za\u010del zaznavati dim", + "sound": "{entity_name} za\u010del zaznavati zvok", + "turned_off": "{entity_name} izklopljen", + "turned_on": "{entity_name} vklopljen", + "unsafe": "{entity_name} je postal nevaren", + "vibration": "{entity_name} je za\u010del odkrivat vibracije" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/sl.json b/homeassistant/components/izone/.translations/sl.json new file mode 100644 index 00000000000000..cb2fe82129741d --- /dev/null +++ b/homeassistant/components/izone/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni najdenih naprav iZone.", + "single_instance_allowed": "Potrebna je samo ena konfiguracija iZone." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/.translations/sensor.no.json b/homeassistant/components/moon/.translations/sensor.no.json index a440fdde4f27ca..3998494606f8de 100644 --- a/homeassistant/components/moon/.translations/sensor.no.json +++ b/homeassistant/components/moon/.translations/sensor.no.json @@ -2,7 +2,7 @@ "state": { "first_quarter": "F\u00f8rste kvartal", "full_moon": "Fullm\u00e5ne", - "last_quarter": "Siste kvarter", + "last_quarter": "Siste kvartal", "new_moon": "Nym\u00e5ne", "waning_crescent": "Minkende m\u00e5nesigd", "waning_gibbous": "Minkende m\u00e5ne", diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json index d2610c68aed74f..171c656566d279 100644 --- a/homeassistant/components/plex/.translations/ko.json +++ b/homeassistant/components/plex/.translations/ko.json @@ -24,7 +24,7 @@ "data": { "token": "Plex \ud1a0\ud070" }, - "description": "\uc790\ub3d9 \uc124\uc815\uc744 \uc704\ud574 Plex \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "description": "\uc790\ub3d9 \uc124\uc815\uc744 \uc704\ud574 Plex \ud1a0\ud070\uc744 \uc785\ub825\ud558\uac70\ub098 \uc11c\ubc84\ub97c \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ud574\uc8fc\uc138\uc694.", "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" } }, diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index b906d0d8dc6c19..b424be059f9a5c 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -36,7 +36,7 @@ "manual_setup": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "token": "\u0422\u043e\u043a\u0435\u043d" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d Plex \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", "title": "Plex" } }, diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json new file mode 100644 index 00000000000000..2a03b7d0e8cd3d --- /dev/null +++ b/homeassistant/components/plex/.translations/sl.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "all_configured": "Vsi povezani stre\u017eniki so \u017ee konfigurirani", + "already_configured": "Ta stre\u017enik Plex je \u017ee konfiguriran", + "already_in_progress": "Plex se konfigurira", + "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", + "unknown": "Ni uspelo iz neznanega razloga" + }, + "error": { + "faulty_credentials": "Avtorizacija ni uspela", + "no_servers": "Ni stre\u017enikov povezanih z ra\u010dunom", + "no_token": "Vnesite \u017eeton ali izberite ro\u010dno nastavitev", + "not_found": "Plex stre\u017enika ni mogo\u010de najti" + }, + "step": { + "manual_setup": { + "data": { + "host": "Gostitelj", + "port": "Vrata", + "ssl": "Uporaba SSL", + "token": "\u017deton (po potrebi)", + "verify_ssl": "Preverite SSL potrdilo" + }, + "title": "Plex stre\u017enik" + }, + "select_server": { + "data": { + "server": "Stre\u017enik" + }, + "description": "Na voljo je ve\u010d stre\u017enikov, izberite enega:", + "title": "Izberite stre\u017enik Plex" + }, + "user": { + "data": { + "manual_setup": "Ro\u010dna nastavitev", + "token": "Plex \u017eeton" + }, + "description": "Vnesite \u017eeton Plex za samodejno nastavitev ali ro\u010dno konfigurirajte stre\u017enik.", + "title": "Pove\u017eite stre\u017enik Plex" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/en.json b/homeassistant/components/zha/.translations/en.json index 6a819fbc16f8e2..84b335bdeaac95 100644 --- a/homeassistant/components/zha/.translations/en.json +++ b/homeassistant/components/zha/.translations/en.json @@ -1,63 +1,63 @@ { - "config": { - "abort": { - "single_instance_allowed": "Only a single configuration of ZHA is allowed." - }, - "error": { - "cannot_connect": "Unable to connect to ZHA device." - }, - "step": { - "user": { - "data": { - "radio_type": "Radio Type", - "usb_path": "USB Device Path" + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of ZHA is allowed." + }, + "error": { + "cannot_connect": "Unable to connect to ZHA device." + }, + "step": { + "user": { + "data": { + "radio_type": "Radio Type", + "usb_path": "USB Device Path" + }, + "title": "ZHA" + } }, "title": "ZHA" - } - }, - "title": "ZHA" - }, - "device_automation": { - "trigger_type": { - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "device_rotated": "Device rotated \"{subtype}\"", - "device_shaken": "Device shaken", - "device_slid": "Device slid \"{subtype}\"", - "device_tilted": "Device tilted", - "device_knocked": "Device knocked \"{subtype}\"", - "device_dropped": "Device dropped", - "device_flipped": "Device flipped \"{subtype}\"" }, - "trigger_subtype": { - "turn_on": "Turn on", - "turn_off": "Turn off", - "dim_up": "Dim up", - "dim_down": "Dim down", - "left": "Left", - "right": "Right", - "open": "Open", - "close": "Close", - "both_buttons": "Both buttons", - "button_1": "First button", - "button_2": "Second button", - "button_3": "Third button", - "button_4": "Fourth button", - "button_5": "Fifth button", - "button_6": "Sixth button", - "face_any": "With any/specified face(s) activated", - "face_1": "With face 1 activated", - "face_2": "With face 2 activated", - "face_3": "With face 3 activated", - "face_4": "With face 4 activated", - "face_5": "With face 5 activated", - "face_6": "With face 6 activated" + "device_automation": { + "trigger_subtype": { + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "close": "Close", + "dim_down": "Dim down", + "dim_up": "Dim up", + "face_1": "with face 1 activated", + "face_2": "with face 2 activated", + "face_3": "with face 3 activated", + "face_4": "with face 4 activated", + "face_5": "with face 5 activated", + "face_6": "with face 6 activated", + "face_any": "With any/specified face(s) activated", + "left": "Left", + "open": "Open", + "right": "Right", + "turn_off": "Turn off", + "turn_on": "Turn on" + }, + "trigger_type": { + "device_dropped": "Device dropped", + "device_flipped": "Device flipped \"{subtype}\"", + "device_knocked": "Device knocked \"{subtype}\"", + "device_rotated": "Device rotated \"{subtype}\"", + "device_shaken": "Device shaken", + "device_slid": "Device slid \"{subtype}\"", + "device_tilted": "Device tilted", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_triple_press": "\"{subtype}\" button triple clicked" + } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 9db55494ba4ac2..f639c85c682612 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -16,5 +16,26 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Begge knapper", + "button_1": "F\u00f8rste knapp", + "button_2": "Andre knapp", + "button_3": "Tredje knapp", + "button_4": "Fjerde knapp", + "close": "Lukk", + "dim_down": "Dimm ned", + "dim_up": "Dimm opp", + "left": "Venstre", + "open": "\u00c5pen", + "right": "H\u00f8yre", + "turn_off": "Skru av", + "turn_on": "Sl\u00e5 p\u00e5" + }, + "trigger_type": { + "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", + "remote_button_short_release": "\"{subtype}\"-knappen sluppet" + } } } \ No newline at end of file From 51a090cfdc99ea2502f9fb976b8cc1b58189cce4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 Sep 2019 20:47:24 -0700 Subject: [PATCH 149/296] Fix CI --- requirements_test.txt | 1 + requirements_test_all.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements_test.txt b/requirements_test.txt index b9b919c4bfd077..ae4401178b1809 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,6 +13,7 @@ mypy==0.720 pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 +astroid==2.2.5 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63ad27a654caa7..c92b16b78fe566 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,6 +14,7 @@ mypy==0.720 pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 +astroid==2.2.5 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 From 31964d0f49467e99e7d9a87811c7d50ce3fda63b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 25 Sep 2019 00:01:39 -0400 Subject: [PATCH 150/296] bump quirks (#26885) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 7744b9f223339f..f46904f326721b 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.10.0", - "zha-quirks==0.0.23", + "zha-quirks==0.0.25", "zigpy-deconz==0.4.0", "zigpy-homeassistant==0.9.0", "zigpy-xbee-homeassistant==0.5.0", diff --git a/requirements_all.txt b/requirements_all.txt index b8d4a158655af8..3a40af2b1afd6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2011,7 +2011,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.23 +zha-quirks==0.0.25 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From dc229ed56970a438fb4a24573366f18925a7d3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Wed, 25 Sep 2019 06:02:23 +0200 Subject: [PATCH 151/296] Update zigpy_zigate to 0.4.0 (#26883) * zigpy-zigate==0.4.0 * zigpy-zigate==0.4.0 --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index f46904f326721b..c4de1d66e838b7 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.4.0", "zigpy-homeassistant==0.9.0", "zigpy-xbee-homeassistant==0.5.0", - "zigpy-zigate==0.3.1" + "zigpy-zigate==0.4.0" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index 3a40af2b1afd6e..2b9fc631994dd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2029,7 +2029,7 @@ zigpy-homeassistant==0.9.0 zigpy-xbee-homeassistant==0.5.0 # homeassistant.components.zha -zigpy-zigate==0.3.1 +zigpy-zigate==0.4.0 # homeassistant.components.zoneminder zm-py==0.3.3 From 2ffbe5b99f5565aeade75276994fb537a8f48039 Mon Sep 17 00:00:00 2001 From: tleegaard Date: Wed, 25 Sep 2019 06:16:08 +0200 Subject: [PATCH 152/296] Inverting states for opening/closing Homekit covers (#26872) * Update cover.py * Update test_cover.py --- homeassistant/components/homekit_controller/cover.py | 2 +- tests/components/homekit_controller/test_cover.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 7f70b0cfac0f92..0606778acb5768 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -33,7 +33,7 @@ TARGET_GARAGE_STATE_MAP = {STATE_OPEN: 0, STATE_CLOSED: 1, STATE_STOPPED: 2} -CURRENT_WINDOW_STATE_MAP = {0: STATE_OPENING, 1: STATE_CLOSING, 2: STATE_STOPPED} +CURRENT_WINDOW_STATE_MAP = {0: STATE_CLOSING, 1: STATE_OPENING, 2: STATE_STOPPED} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/tests/components/homekit_controller/test_cover.py b/tests/components/homekit_controller/test_cover.py index afbb55e03f3230..53245176a04e34 100644 --- a/tests/components/homekit_controller/test_cover.py +++ b/tests/components/homekit_controller/test_cover.py @@ -93,11 +93,11 @@ async def test_read_window_cover_state(hass, utcnow): helper.characteristics[POSITION_STATE].value = 0 state = await helper.poll_and_get_state() - assert state.state == "opening" + assert state.state == "closing" helper.characteristics[POSITION_STATE].value = 1 state = await helper.poll_and_get_state() - assert state.state == "closing" + assert state.state == "opening" helper.characteristics[POSITION_STATE].value = 2 state = await helper.poll_and_get_state() From 87f1f6cc9fde6b1628dae3073e615e1aa8ebff4d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 Sep 2019 06:28:50 +0200 Subject: [PATCH 153/296] Removes unnecessary utf8 source encoding declarations (#26887) --- homeassistant/components/lcn/const.py | 1 - homeassistant/components/yandex_transport/sensor.py | 1 - homeassistant/const.py | 1 - 3 files changed, 3 deletions(-) diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index ecef388c0d4aa5..c49319abf420cf 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -1,4 +1,3 @@ -# coding: utf-8 """Constants for the LCN component.""" from itertools import product diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 340291807ead98..26311a4c72e58c 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Service for obtaining information about closer bus from Transport Yandex Service.""" import logging diff --git a/homeassistant/const.py b/homeassistant/const.py index 26cb3c20942666..9aa4544f5c34e7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,4 +1,3 @@ -# coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 From 1f03508bfe92602267933fa2afcdb9f936fafbc0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 Sep 2019 06:29:57 +0200 Subject: [PATCH 154/296] Removes unnecessary print_function future import (#26888) --- homeassistant/__main__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index f7e24d69884975..b416b1f98d3d1b 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,6 +1,4 @@ """Start Home Assistant.""" -from __future__ import print_function - import argparse import os import platform From cd976b65ae2dba92556da0211563972f34c99645 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Wed, 25 Sep 2019 14:30:48 +1000 Subject: [PATCH 155/296] Add availability_template to Template Switch platform (#26513) * Added availability_template to Template Switch platform * Fixed Entity discovery big and coverage * flake8 * Cleaned template setup * I'll remember to run black every time one of these days... * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Refactored availability_tempalte rendering to common loop * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (async, magic values and state checks) * Fixed Enity Extraction --- homeassistant/components/template/switch.py | 73 +++++++++++++++----- tests/components/template/test_switch.py | 75 ++++++++++++++++++++- 2 files changed, 130 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index c77a90c1f8b6ee..2d4dda032ca857 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -19,12 +19,14 @@ ATTR_ENTITY_ID, CONF_SWITCHES, EVENT_HOMEASSISTANT_START, + MATCH_ALL, ) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] @@ -37,6 +39,7 @@ vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, @@ -58,19 +61,47 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config[CONF_VALUE_TEMPLATE] icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[ON_ACTION] off_action = device_config[OFF_ACTION] - entity_ids = ( - device_config.get(ATTR_ENTITY_ID) or state_template.extract_entities() - ) - - state_template.hass = hass - - if icon_template is not None: - icon_template.hass = hass - - if entity_picture_template is not None: - entity_picture_template.hass = hass + manual_entity_ids = device_config.get(ATTR_ENTITY_ID) + entity_ids = set() + + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + } + invalid_templates = [] + + for template_name, template in templates.items(): + if template is not None: + template.hass = hass + + if manual_entity_ids is not None: + continue + + template_entity_ids = template.extract_entities() + if template_entity_ids == MATCH_ALL: + invalid_templates.append(template_name.replace("_template", "")) + entity_ids = MATCH_ALL + elif entity_ids != MATCH_ALL: + entity_ids |= set(template_entity_ids) + if invalid_templates: + _LOGGER.warning( + "Template sensor %s has no entity ids configured to track nor" + " were we able to extract the entities to track from the %s " + "template(s). This entity will only be able to be updated " + "manually.", + device, + ", ".join(invalid_templates), + ) + else: + if manual_entity_ids is None: + entity_ids = list(entity_ids) + else: + entity_ids = manual_entity_ids switches.append( SwitchTemplate( @@ -80,6 +111,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, entity_ids, @@ -104,6 +136,7 @@ def __init__( state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, entity_ids, @@ -120,9 +153,11 @@ def __init__( self._state = False self._icon_template = icon_template self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._icon = None self._entity_picture = None self._entities = entity_ids + self._available = True async def async_added_to_hass(self): """Register callbacks.""" @@ -160,11 +195,6 @@ def should_poll(self): """Return the polling state.""" return False - @property - def available(self): - """If switch is available.""" - return self._state is not None - @property def icon(self): """Return the icon to use in the frontend, if any.""" @@ -175,6 +205,11 @@ def entity_picture(self): """Return the entity_picture to use in the frontend, if any.""" return self._entity_picture + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_turn_on(self, **kwargs): """Fire the on action.""" await self._on_script.async_run(context=self._context) @@ -205,12 +240,16 @@ async def async_update(self): for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), + ("_available", self._availability_template), ): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index 9a07a935d12546..3adc5dcad46db9 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -1,7 +1,7 @@ """The tests for the Template switch platform.""" from homeassistant.core import callback from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from tests.common import get_test_home_assistant, assert_setup_component from tests.components.switch import common @@ -474,3 +474,76 @@ def test_off_action(self): self.hass.block_till_done() assert len(self.calls) == 1 + + +async def test_available_template_with_entities(hass): + """Test availability templates with values from other entities.""" + await setup.async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "template", + "switches": { + "test_template_switch": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "turn_off": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + "availability_template": "{{ is_state('availability_state.state', 'on') }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_template_switch").state != STATE_UNAVAILABLE + + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_template_switch").state == STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + await setup.async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "template", + "switches": { + "test_template_switch": { + "value_template": "{{ true }}", + "turn_on": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "turn_off": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + "availability_template": "{{ x - 12 }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("switch.test_template_switch").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text From aaf013da6e99be41ad63f584162f22936cc304d2 Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Wed, 25 Sep 2019 15:20:46 +0700 Subject: [PATCH 156/296] Bump ndms2-client to 0.0.9 (#26899) --- homeassistant/components/keenetic_ndms2/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 42d8d89a021152..417616161e56ea 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -3,7 +3,7 @@ "name": "Keenetic ndms2", "documentation": "https://www.home-assistant.io/components/keenetic_ndms2", "requirements": [ - "ndms2_client==0.0.8" + "ndms2_client==0.0.9" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 2b9fc631994dd1..2f8ae35125d807 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -841,7 +841,7 @@ n26==0.2.7 nad_receiver==0.0.11 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.0.8 +ndms2_client==0.0.9 # homeassistant.components.ness_alarm nessclient==0.9.15 From f5018e91b586381a0035c17330faa25cd3ba1d7b Mon Sep 17 00:00:00 2001 From: zhumuht <40521367+zhumuht@users.noreply.github.com> Date: Wed, 25 Sep 2019 21:04:27 +0800 Subject: [PATCH 157/296] Add voltage attribute to Xiaomi Aqara devices (#26876) --- homeassistant/components/xiaomi_aqara/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 6e2298e05b9782..26d0f351fa298b 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -8,6 +8,7 @@ from homeassistant.components.discovery import SERVICE_XIAOMI_GW from homeassistant.const import ( ATTR_BATTERY_LEVEL, + ATTR_VOLTAGE, CONF_HOST, CONF_MAC, CONF_PORT, @@ -323,6 +324,7 @@ def parse_voltage(self, data): max_volt = 3300 min_volt = 2800 voltage = data[voltage_key] + self._device_state_attributes[ATTR_VOLTAGE] = round(voltage/1000.0, 2) voltage = min(voltage, max_volt) voltage = max(voltage, min_volt) percent = ((voltage - min_volt) / (max_volt - min_volt)) * 100 From 626b61b58f97bf3b7b15009d110e0dcf28c33e31 Mon Sep 17 00:00:00 2001 From: zhumuht <40521367+zhumuht@users.noreply.github.com> Date: Wed, 25 Sep 2019 21:39:01 +0800 Subject: [PATCH 158/296] Fix bed_activity history chart of the Xiaomi Aqara vibration sensor (#26875) --- homeassistant/components/xiaomi_aqara/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index b3f29e93c639ce..5ad29af0aaf399 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -19,6 +19,7 @@ "illumination": ["lm", None, DEVICE_CLASS_ILLUMINANCE], "lux": ["lx", None, DEVICE_CLASS_ILLUMINANCE], "pressure": ["hPa", None, DEVICE_CLASS_PRESSURE], + "bed_activity": ["μm", None, None], } From cc611615aa4e5bd34c5514ab7f7484e2a5f11a47 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 25 Sep 2019 09:04:34 -0700 Subject: [PATCH 159/296] Fix missing whitespace around arithmetic operator (#26908) --- homeassistant/components/xiaomi_aqara/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 26d0f351fa298b..7a337dcc497a81 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -324,7 +324,7 @@ def parse_voltage(self, data): max_volt = 3300 min_volt = 2800 voltage = data[voltage_key] - self._device_state_attributes[ATTR_VOLTAGE] = round(voltage/1000.0, 2) + self._device_state_attributes[ATTR_VOLTAGE] = round(voltage / 1000.0, 2) voltage = min(voltage, max_volt) voltage = max(voltage, min_volt) percent = ((voltage - min_volt) / (max_volt - min_volt)) * 100 From 4582b6e668b4d347fe1d96ed1d3d454243840299 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 25 Sep 2019 18:56:31 +0200 Subject: [PATCH 160/296] deCONZ - Improve ssdp discovery by storing uuid in config entry (#26882) * Improve ssdp discovery by storing uuid in config entry so discovery can update any deconz entry, loaded or not --- homeassistant/components/deconz/__init__.py | 18 ++++++++---- .../components/deconz/config_flow.py | 17 +++++------ homeassistant/components/deconz/const.py | 4 ++- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_config_flow.py | 22 ++++++++------ tests/components/deconz/test_gateway.py | 3 +- tests/components/deconz/test_init.py | 29 ++++++++++--------- 9 files changed, 57 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 558b0fe4205147..0ea91d10b191bd 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -4,8 +4,8 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from .config_flow import get_master_gateway -from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DOMAIN -from .gateway import DeconzGateway +from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, CONF_UUID, DOMAIN +from .gateway import DeconzGateway, get_gateway_from_config_entry from .services import async_setup_services, async_unload_services CONFIG_SCHEMA = vol.Schema( @@ -39,6 +39,9 @@ async def async_setup_entry(hass, config_entry): await gateway.async_update_device_registry() + if CONF_UUID not in config_entry.data: + await async_add_uuid_to_config_entry(hass, config_entry) + await async_setup_services(hass) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) @@ -68,11 +71,14 @@ async def async_update_master_gateway(hass, config_entry): Makes sure there is always one master available. """ master = not get_master_gateway(hass) + options = {**config_entry.options, CONF_MASTER_GATEWAY: master} - old_options = dict(config_entry.options) + hass.config_entries.async_update_entry(config_entry, options=options) - new_options = {CONF_MASTER_GATEWAY: master} - options = {**old_options, **new_options} +async def async_add_uuid_to_config_entry(hass, config_entry): + """Add UUID to config entry to help discovery identify entries.""" + gateway = get_gateway_from_config_entry(hass, config_entry) + config = {**config_entry.data, CONF_UUID: gateway.api.config.uuid} - hass.config_entries.async_update_entry(config_entry, options=options) + hass.config_entries.async_update_entry(config_entry, data=config) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index c63b1721393222..488d48bb7409d8 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -5,7 +5,7 @@ import voluptuous as vol from pydeconz.errors import ResponseError, RequestError -from pydeconz.utils import async_discovery, async_get_api_key, async_get_bridgeid +from pydeconz.utils import async_discovery, async_get_api_key, async_get_gateway_config from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT @@ -16,6 +16,7 @@ CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, + CONF_UUID, DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, @@ -144,9 +145,11 @@ async def _create_entry(self): try: with async_timeout.timeout(10): - self.deconz_config[CONF_BRIDGEID] = await async_get_bridgeid( + gateway_config = await async_get_gateway_config( session, **self.deconz_config ) + self.deconz_config[CONF_BRIDGEID] = gateway_config.bridgeid + self.deconz_config[CONF_UUID] = gateway_config.uuid except asyncio.TimeoutError: return self.async_abort(reason="no_bridges") @@ -172,14 +175,10 @@ async def async_step_ssdp(self, discovery_info): return self.async_abort(reason="not_deconz_bridge") uuid = discovery_info[ATTR_UUID].replace("uuid:", "") - gateways = { - gateway.api.config.uuid: gateway - for gateway in self.hass.data.get(DOMAIN, {}).values() - } - if uuid in gateways: - entry = gateways[uuid].config_entry - return await self._update_entry(entry, discovery_info[CONF_HOST]) + for entry in self.hass.config_entries.async_entries(DOMAIN): + if uuid == entry.data.get(CONF_UUID): + return await self._update_entry(entry, discovery_info[CONF_HOST]) bridgeid = discovery_info[ATTR_SERIAL] if any( diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 62879a82724b03..ad23a564272d9c 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -5,13 +5,15 @@ DOMAIN = "deconz" +CONF_BRIDGEID = "bridgeid" +CONF_UUID = "uuid" + DEFAULT_PORT = 80 DEFAULT_ALLOW_CLIP_SENSOR = False DEFAULT_ALLOW_DECONZ_GROUPS = True CONF_ALLOW_CLIP_SENSOR = "allow_clip_sensor" CONF_ALLOW_DECONZ_GROUPS = "allow_deconz_groups" -CONF_BRIDGEID = "bridgeid" CONF_MASTER_GATEWAY = "master" SUPPORTED_PLATFORMS = [ diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index fe8f49d260a93f..4aec29008dee80 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/deconz", "requirements": [ - "pydeconz==62" + "pydeconz==63" ], "ssdp": { "manufacturer": [ diff --git a/requirements_all.txt b/requirements_all.txt index 2f8ae35125d807..73ececb0517b6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1129,7 +1129,7 @@ pydaikin==1.6.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==62 +pydeconz==63 # homeassistant.components.delijn pydelijn==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c92b16b78fe566..6f542147363915 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -294,7 +294,7 @@ pyblackbird==0.5 pychromecast==4.0.1 # homeassistant.components.deconz -pydeconz==62 +pydeconz==63 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index d7071d6daef08c..4d8d3a312582a1 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -209,22 +209,25 @@ async def test_bridge_discovery_update_existing_entry(hass): """Test if a discovered bridge has already been configured.""" entry = MockConfigEntry( domain=config_flow.DOMAIN, - data={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_BRIDGEID: "id"}, + data={ + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_BRIDGEID: "123ABC", + config_flow.CONF_UUID: "456DEF", + }, ) entry.add_to_hass(hass) gateway = Mock() gateway.config_entry = entry - gateway.api.config.uuid = "1234" - hass.data[config_flow.DOMAIN] = {"id": gateway} + hass.data[config_flow.DOMAIN] = {"123ABC": gateway} result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={ config_flow.CONF_HOST: "mock-deconz", - ATTR_SERIAL: "id", + ATTR_SERIAL: "123ABC", ATTR_MANUFACTURERURL: config_flow.DECONZ_MANUFACTURERURL, - config_flow.ATTR_UUID: "uuid:1234", + config_flow.ATTR_UUID: "uuid:456DEF", }, context={"source": "ssdp"}, ) @@ -238,7 +241,7 @@ async def test_create_entry(hass, aioclient_mock): """Test that _create_entry work and that bridgeid can be requested.""" aioclient_mock.get( "http://1.2.3.4:80/api/1234567890ABCDEF/config", - json={"bridgeid": "id"}, + json={"bridgeid": "123ABC", "uuid": "456DEF"}, headers={"content-type": "application/json"}, ) @@ -253,12 +256,13 @@ async def test_create_entry(hass, aioclient_mock): result = await flow._create_entry() assert result["type"] == "create_entry" - assert result["title"] == "deCONZ-id" + assert result["title"] == "deCONZ-123ABC" assert result["data"] == { - config_flow.CONF_BRIDGEID: "id", + config_flow.CONF_BRIDGEID: "123ABC", config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 80, config_flow.CONF_API_KEY: "1234567890ABCDEF", + config_flow.CONF_UUID: "456DEF", } @@ -273,7 +277,7 @@ async def test_create_entry_timeout(hass, aioclient_mock): } with patch( - "homeassistant.components.deconz.config_flow.async_get_bridgeid", + "homeassistant.components.deconz.config_flow.async_get_gateway_config", side_effect=asyncio.TimeoutError, ): result = await flow._create_entry() diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 25a1cd465c510a..b98681b6fc9697 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -20,6 +20,7 @@ deconz.config_flow.CONF_BRIDGEID: BRIDGEID, deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, + deconz.config_flow.CONF_UUID: "456DEF", } DECONZ_CONFIG = { @@ -147,7 +148,7 @@ async def test_update_address(hass): deconz.config_flow.CONF_PORT: 80, ssdp.ATTR_SERIAL: BRIDGEID, ssdp.ATTR_MANUFACTURERURL: deconz.config_flow.DECONZ_MANUFACTURERURL, - deconz.config_flow.ATTR_UUID: "uuid:1234", + deconz.config_flow.ATTR_UUID: "uuid:456DEF", }, context={"source": "ssdp"}, ) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 7d630498cde14e..986e01a1599bc9 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,33 +1,34 @@ """Test deCONZ component setup process.""" -from unittest.mock import Mock, patch - import asyncio + +from asynctest import Mock, patch + import pytest from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components import deconz -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry ENTRY1_HOST = "1.2.3.4" ENTRY1_PORT = 80 ENTRY1_API_KEY = "1234567890ABCDEF" ENTRY1_BRIDGEID = "12345ABC" +ENTRY1_UUID = "456DEF" ENTRY2_HOST = "2.3.4.5" ENTRY2_PORT = 80 ENTRY2_API_KEY = "1234567890ABCDEF" ENTRY2_BRIDGEID = "23456DEF" +ENTRY2_UUID = "789ACE" async def setup_entry(hass, entry): """Test that setup entry works.""" with patch.object( - deconz.DeconzGateway, "async_setup", return_value=mock_coro(True) + deconz.DeconzGateway, "async_setup", return_value=True ), patch.object( - deconz.DeconzGateway, - "async_update_device_registry", - return_value=mock_coro(True), + deconz.DeconzGateway, "async_update_device_registry", return_value=True ): assert await deconz.async_setup_entry(hass, entry) is True @@ -67,6 +68,7 @@ async def test_setup_entry_successful(hass): deconz.config_flow.CONF_PORT: ENTRY1_PORT, deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, + deconz.CONF_UUID: ENTRY1_UUID, }, ) entry.add_to_hass(hass) @@ -86,6 +88,7 @@ async def test_setup_entry_multiple_gateways(hass): deconz.config_flow.CONF_PORT: ENTRY1_PORT, deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, + deconz.CONF_UUID: ENTRY1_UUID, }, ) entry.add_to_hass(hass) @@ -97,6 +100,7 @@ async def test_setup_entry_multiple_gateways(hass): deconz.config_flow.CONF_PORT: ENTRY2_PORT, deconz.config_flow.CONF_API_KEY: ENTRY2_API_KEY, deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID, + deconz.CONF_UUID: ENTRY2_UUID, }, ) entry2.add_to_hass(hass) @@ -119,15 +123,14 @@ async def test_unload_entry(hass): deconz.config_flow.CONF_PORT: ENTRY1_PORT, deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, + deconz.CONF_UUID: ENTRY1_UUID, }, ) entry.add_to_hass(hass) await setup_entry(hass, entry) - with patch.object( - deconz.DeconzGateway, "async_reset", return_value=mock_coro(True) - ): + with patch.object(deconz.DeconzGateway, "async_reset", return_value=True): assert await deconz.async_unload_entry(hass, entry) assert not hass.data[deconz.DOMAIN] @@ -142,6 +145,7 @@ async def test_unload_entry_multiple_gateways(hass): deconz.config_flow.CONF_PORT: ENTRY1_PORT, deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, + deconz.CONF_UUID: ENTRY1_UUID, }, ) entry.add_to_hass(hass) @@ -153,6 +157,7 @@ async def test_unload_entry_multiple_gateways(hass): deconz.config_flow.CONF_PORT: ENTRY2_PORT, deconz.config_flow.CONF_API_KEY: ENTRY2_API_KEY, deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID, + deconz.CONF_UUID: ENTRY2_UUID, }, ) entry2.add_to_hass(hass) @@ -160,9 +165,7 @@ async def test_unload_entry_multiple_gateways(hass): await setup_entry(hass, entry) await setup_entry(hass, entry2) - with patch.object( - deconz.DeconzGateway, "async_reset", return_value=mock_coro(True) - ): + with patch.object(deconz.DeconzGateway, "async_reset", return_value=True): assert await deconz.async_unload_entry(hass, entry) assert ENTRY2_BRIDGEID in hass.data[deconz.DOMAIN] From d1adb28c6b1f08e61f07fbb4b45addcc8ee3152f Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Wed, 25 Sep 2019 20:13:31 +0300 Subject: [PATCH 161/296] Add google_assistant alarm_control_panel (#26249) * add alarm_control_panel to google_assistant * add cancel arming option * raise error if requested state is same as current * rework executing command logic * Add tests * fixed tests * fixed level synonyms --- .../components/google_assistant/const.py | 7 + .../components/google_assistant/trait.py | 112 +++++- tests/components/google_assistant/__init__.py | 7 + .../google_assistant/test_google_assistant.py | 18 +- .../components/google_assistant/test_trait.py | 337 +++++++++++++++++- 5 files changed, 478 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 1d266d23d3fccd..54abd54caaffc6 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -15,6 +15,7 @@ sensor, switch, vacuum, + alarm_control_panel, ) DOMAIN = "google_assistant" @@ -48,6 +49,7 @@ "lock", "binary_sensor", "sensor", + "alarm_control_panel", ] PREFIX_TYPES = "action.devices.types." @@ -66,6 +68,7 @@ TYPE_DOOR = PREFIX_TYPES + "DOOR" TYPE_TV = PREFIX_TYPES + "TV" TYPE_SPEAKER = PREFIX_TYPES + "SPEAKER" +TYPE_ALARM = PREFIX_TYPES + "SECURITYSYSTEM" SERVICE_REQUEST_SYNC = "request_sync" HOMEGRAPH_URL = "https://homegraph.googleapis.com/" @@ -81,6 +84,9 @@ ERR_UNKNOWN_ERROR = "unknownError" ERR_FUNCTION_NOT_SUPPORTED = "functionNotSupported" +ERR_ALREADY_DISARMED = "alreadyDisarmed" +ERR_ALREADY_ARMED = "alreadyArmed" + ERR_CHALLENGE_NEEDED = "challengeNeeded" ERR_CHALLENGE_NOT_SETUP = "challengeFailedNotSetup" ERR_TOO_MANY_FAILED_ATTEMPTS = "tooManyFailedAttempts" @@ -106,6 +112,7 @@ script.DOMAIN: TYPE_SCENE, switch.DOMAIN: TYPE_SWITCH, vacuum.DOMAIN: TYPE_VACUUM, + alarm_control_panel.DOMAIN: TYPE_ALARM, } DEVICE_CLASS_TO_GOOGLE_TYPES = { diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 2afa18af32e6a7..7d6e79a82372be 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -16,6 +16,7 @@ sensor, switch, vacuum, + alarm_control_panel, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( @@ -31,6 +32,20 @@ ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, ATTR_ASSUMED_STATE, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_TRIGGER, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, + STATE_ALARM_PENDING, + ATTR_CODE, STATE_UNKNOWN, ) from homeassistant.core import DOMAIN as HA_DOMAIN @@ -43,6 +58,8 @@ CHALLENGE_ACK_NEEDED, CHALLENGE_PIN_NEEDED, CHALLENGE_FAILED_PIN_NEEDED, + ERR_ALREADY_DISARMED, + ERR_ALREADY_ARMED, ) from .error import SmartHomeError, ChallengeNeeded @@ -62,6 +79,7 @@ TRAIT_MODES = PREFIX_TRAITS + "Modes" TRAIT_OPENCLOSE = PREFIX_TRAITS + "OpenClose" TRAIT_VOLUME = PREFIX_TRAITS + "Volume" +TRAIT_ARMDISARM = PREFIX_TRAITS + "ArmDisarm" PREFIX_COMMANDS = "action.devices.commands." COMMAND_ONOFF = PREFIX_COMMANDS + "OnOff" @@ -85,6 +103,7 @@ COMMAND_OPENCLOSE = PREFIX_COMMANDS + "OpenClose" COMMAND_SET_VOLUME = PREFIX_COMMANDS + "setVolume" COMMAND_VOLUME_RELATIVE = PREFIX_COMMANDS + "volumeRelative" +COMMAND_ARMDISARM = PREFIX_COMMANDS + "ArmDisarm" TRAITS = [] @@ -873,6 +892,98 @@ async def execute(self, command, data, params, challenge): ) +@register_trait +class ArmDisArmTrait(_Trait): + """Trait to Arm or Disarm a Security System. + + https://developers.google.com/actions/smarthome/traits/armdisarm + """ + + name = TRAIT_ARMDISARM + commands = [COMMAND_ARMDISARM] + + state_to_service = { + STATE_ALARM_ARMED_HOME: SERVICE_ALARM_ARM_HOME, + STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY, + STATE_ALARM_ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT, + STATE_ALARM_ARMED_CUSTOM_BYPASS: SERVICE_ALARM_ARM_CUSTOM_BYPASS, + STATE_ALARM_TRIGGERED: SERVICE_ALARM_TRIGGER, + } + + @staticmethod + def supported(domain, features, device_class): + """Test if state is supported.""" + return domain == alarm_control_panel.DOMAIN + + @staticmethod + def might_2fa(domain, features, device_class): + """Return if the trait might ask for 2FA.""" + return True + + def sync_attributes(self): + """Return ArmDisarm attributes for a sync request.""" + response = {} + levels = [] + for state in self.state_to_service: + # level synonyms are generated from state names + # 'armed_away' becomes 'armed away' or 'away' + level_synonym = [state.replace("_", " ")] + if state != STATE_ALARM_TRIGGERED: + level_synonym.append(state.split("_")[1]) + + level = { + "level_name": state, + "level_values": [{"level_synonym": level_synonym, "lang": "en"}], + } + levels.append(level) + response["availableArmLevels"] = {"levels": levels, "ordered": False} + return response + + def query_attributes(self): + """Return ArmDisarm query attributes.""" + if "post_pending_state" in self.state.attributes: + armed_state = self.state.attributes["post_pending_state"] + else: + armed_state = self.state.state + response = {"isArmed": armed_state in self.state_to_service} + if response["isArmed"]: + response.update({"currentArmLevel": armed_state}) + return response + + async def execute(self, command, data, params, challenge): + """Execute an ArmDisarm command.""" + if params["arm"] and not params.get("cancel"): + if self.state.state == params["armLevel"]: + raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed") + if self.state.attributes["code_arm_required"]: + _verify_pin_challenge(data, self.state, challenge) + service = self.state_to_service[params["armLevel"]] + # disarm the system without asking for code when + # 'cancel' arming action is received while current status is pending + elif ( + params["arm"] + and params.get("cancel") + and self.state.state == STATE_ALARM_PENDING + ): + service = SERVICE_ALARM_DISARM + else: + if self.state.state == STATE_ALARM_DISARMED: + raise SmartHomeError(ERR_ALREADY_DISARMED, "System is already disarmed") + _verify_pin_challenge(data, self.state, challenge) + service = SERVICE_ALARM_DISARM + + await self.hass.services.async_call( + alarm_control_panel.DOMAIN, + service, + { + ATTR_ENTITY_ID: self.state.entity_id, + ATTR_CODE: data.config.secure_devices_pin, + }, + blocking=True, + context=data.context, + ) + + @register_trait class FanSpeedTrait(_Trait): """Trait to control speed of Fan. @@ -1343,7 +1454,6 @@ def _verify_pin_challenge(data, state, challenge): """Verify a pin challenge.""" if not data.config.should_2fa(state): return - if not data.config.secure_devices_pin: raise SmartHomeError(ERR_CHALLENGE_NOT_SETUP, "Challenge is not set up") diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index ccb74e88e37329..12de2eaba1cdc9 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -230,4 +230,11 @@ def should_expose(self, state): "type": "action.devices.types.LOCK", "willReportState": False, }, + { + "id": "alarm_control_panel.alarm", + "name": {"name": "Alarm"}, + "traits": ["action.devices.traits.ArmDisarm"], + "type": "action.devices.types.SECURITYSYSTEM", + "willReportState": False, + }, ] diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 6a7b69daabb43e..6473e8964b874c 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -7,7 +7,15 @@ import pytest from homeassistant import core, const, setup -from homeassistant.components import fan, cover, light, switch, lock, media_player +from homeassistant.components import ( + fan, + cover, + light, + switch, + lock, + media_player, + alarm_control_panel, +) from homeassistant.components.climate import const as climate from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.components import google_assistant as ga @@ -98,6 +106,14 @@ def hass_fixture(loop, hass): setup.async_setup_component(hass, lock.DOMAIN, {"lock": [{"platform": "demo"}]}) ) + loop.run_until_complete( + setup.async_setup_component( + hass, + alarm_control_panel.DOMAIN, + {"alarm_control_panel": [{"platform": "demo"}]}, + ) + ) + return hass diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 0288aa87572e96..a5c527dacfe6e2 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,6 +1,6 @@ """Tests for the Google Assistant traits.""" from unittest.mock import patch, Mock - +import logging import pytest from homeassistant.components import ( @@ -18,12 +18,16 @@ switch, vacuum, group, + alarm_control_panel, ) from homeassistant.components.climate import const as climate from homeassistant.components.google_assistant import trait, helpers, const, error from homeassistant.const import ( STATE_ON, STATE_OFF, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, @@ -40,6 +44,7 @@ from tests.common import async_mock_service, mock_coro from . import BASIC_CONFIG, MockConfig +_LOGGER = logging.getLogger(__name__) REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" @@ -816,6 +821,336 @@ async def test_lock_unlock_unlock(hass): assert len(calls) == 2 +async def test_arm_disarm_arm_away(hass): + """Test ArmDisarm trait Arming support for alarm_control_panel domain.""" + assert helpers.get_google_type(alarm_control_panel.DOMAIN, None) is not None + assert trait.ArmDisArmTrait.supported(alarm_control_panel.DOMAIN, 0, None) + assert trait.ArmDisArmTrait.might_2fa(alarm_control_panel.DOMAIN, 0, None) + + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) + assert trt.sync_attributes() == { + "availableArmLevels": { + "levels": [ + { + "level_name": "armed_home", + "level_values": [ + {"level_synonym": ["armed home", "home"], "lang": "en"} + ], + }, + { + "level_name": "armed_away", + "level_values": [ + {"level_synonym": ["armed away", "away"], "lang": "en"} + ], + }, + { + "level_name": "armed_night", + "level_values": [ + {"level_synonym": ["armed night", "night"], "lang": "en"} + ], + }, + { + "level_name": "armed_custom_bypass", + "level_values": [ + { + "level_synonym": ["armed custom bypass", "custom"], + "lang": "en", + } + ], + }, + { + "level_name": "triggered", + "level_values": [{"level_synonym": ["triggered"], "lang": "en"}], + }, + ], + "ordered": False, + } + } + + assert trt.query_attributes() == { + "isArmed": True, + "currentArmLevel": STATE_ALARM_ARMED_AWAY, + } + + assert trt.can_execute( + trait.COMMAND_ARMDISARM, {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY} + ) + + calls = async_mock_service( + hass, alarm_control_panel.DOMAIN, alarm_control_panel.SERVICE_ALARM_ARM_AWAY + ) + + # Test with no secure_pin configured + + with pytest.raises(error.SmartHomeError) as err: + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_DISARMED, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + BASIC_CONFIG, + ) + await trt.execute( + trait.COMMAND_ARMDISARM, + BASIC_DATA, + {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}, + {}, + ) + assert len(calls) == 0 + assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP + + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_DISARMED, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) + # No challenge data + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute( + trait.COMMAND_ARMDISARM, + PIN_DATA, + {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}, + {}, + ) + assert len(calls) == 0 + assert err.value.code == const.ERR_CHALLENGE_NEEDED + assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED + + # invalid pin + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute( + trait.COMMAND_ARMDISARM, + PIN_DATA, + {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}, + {"pin": 9999}, + ) + assert len(calls) == 0 + assert err.value.code == const.ERR_CHALLENGE_NEEDED + assert err.value.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED + + # correct pin + await trt.execute( + trait.COMMAND_ARMDISARM, + PIN_DATA, + {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}, + {"pin": "1234"}, + ) + + assert len(calls) == 1 + + # Test already armed + with pytest.raises(error.SmartHomeError) as err: + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) + await trt.execute( + trait.COMMAND_ARMDISARM, + PIN_DATA, + {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}, + {}, + ) + assert len(calls) == 1 + assert err.value.code == const.ERR_ALREADY_ARMED + + # Test with code_arm_required False + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_DISARMED, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False}, + ), + PIN_CONFIG, + ) + await trt.execute( + trait.COMMAND_ARMDISARM, + PIN_DATA, + {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}, + {}, + ) + assert len(calls) == 2 + + +async def test_arm_disarm_disarm(hass): + """Test ArmDisarm trait Disarming support for alarm_control_panel domain.""" + assert helpers.get_google_type(alarm_control_panel.DOMAIN, None) is not None + assert trait.ArmDisArmTrait.supported(alarm_control_panel.DOMAIN, 0, None) + assert trait.ArmDisArmTrait.might_2fa(alarm_control_panel.DOMAIN, 0, None) + + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_DISARMED, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) + assert trt.sync_attributes() == { + "availableArmLevels": { + "levels": [ + { + "level_name": "armed_home", + "level_values": [ + {"level_synonym": ["armed home", "home"], "lang": "en"} + ], + }, + { + "level_name": "armed_away", + "level_values": [ + {"level_synonym": ["armed away", "away"], "lang": "en"} + ], + }, + { + "level_name": "armed_night", + "level_values": [ + {"level_synonym": ["armed night", "night"], "lang": "en"} + ], + }, + { + "level_name": "armed_custom_bypass", + "level_values": [ + { + "level_synonym": ["armed custom bypass", "custom"], + "lang": "en", + } + ], + }, + { + "level_name": "triggered", + "level_values": [{"level_synonym": ["triggered"], "lang": "en"}], + }, + ], + "ordered": False, + } + } + + assert trt.query_attributes() == {"isArmed": False} + + assert trt.can_execute(trait.COMMAND_ARMDISARM, {"arm": False}) + + calls = async_mock_service( + hass, alarm_control_panel.DOMAIN, alarm_control_panel.SERVICE_ALARM_DISARM + ) + + # Test without secure_pin configured + with pytest.raises(error.SmartHomeError) as err: + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + BASIC_CONFIG, + ) + await trt.execute(trait.COMMAND_ARMDISARM, BASIC_DATA, {"arm": False}, {}) + + assert len(calls) == 0 + assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP + + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) + + # No challenge data + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute(trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {}) + assert len(calls) == 0 + assert err.value.code == const.ERR_CHALLENGE_NEEDED + assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED + + # invalid pin + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute( + trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {"pin": 9999} + ) + assert len(calls) == 0 + assert err.value.code == const.ERR_CHALLENGE_NEEDED + assert err.value.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED + + # correct pin + await trt.execute( + trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {"pin": "1234"} + ) + + assert len(calls) == 1 + + # Test already disarmed + with pytest.raises(error.SmartHomeError) as err: + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_DISARMED, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) + await trt.execute(trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {}) + assert len(calls) == 1 + assert err.value.code == const.ERR_ALREADY_DISARMED + + # Cancel arming after already armed will require pin + with pytest.raises(error.SmartHomeError) as err: + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False}, + ), + PIN_CONFIG, + ) + await trt.execute( + trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": True, "cancel": True}, {} + ) + assert len(calls) == 1 + assert err.value.code == const.ERR_CHALLENGE_NEEDED + assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED + + # Cancel arming while pending to arm doesn't require pin + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_PENDING, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False}, + ), + PIN_CONFIG, + ) + await trt.execute( + trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": True, "cancel": True}, {} + ) + assert len(calls) == 2 + + async def test_fan_speed(hass): """Test FanSpeed trait speed control support for fan domain.""" assert helpers.get_google_type(fan.DOMAIN, None) is not None From 36f604f79d5ae38f289899fbb600079b43aced72 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Wed, 25 Sep 2019 11:26:15 -0700 Subject: [PATCH 162/296] Add call direction sensor for Obihai (#26867) * Add call direction sensor for obihai * Check user credentials * Review comments * Fix return --- homeassistant/components/obihai/manifest.json | 2 +- homeassistant/components/obihai/sensor.py | 17 +++++++++++++++++ requirements_all.txt | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index e7706b0435ceec..dd4df479af431c 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -3,7 +3,7 @@ "name": "Obihai", "documentation": "https://www.home-assistant.io/components/obihai", "requirements": [ - "pyobihai==1.1.0" + "pyobihai==1.1.1" ], "dependencies": [], "codeowners": ["@dshokouhi"] diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 4eb3881e95bf37..fbf4fffb17f807 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -46,16 +46,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None): pyobihai = PyObihai() + login = pyobihai.check_account(host, username, password) + if not login: + _LOGGER.error("Invalid credentials") + return + services = pyobihai.get_state(host, username, password) line_services = pyobihai.get_line_state(host, username, password) + call_direction = pyobihai.get_call_direction(host, username, password) + for key in services: sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) for key in line_services: sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + for key in call_direction: + sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + add_entities(sensors) @@ -102,3 +112,10 @@ def update(self): if self._service_name in services: self._state = services.get(self._service_name) + + call_direction = self._pyobihai.get_call_direction( + self._host, self._username, self._password + ) + + if self._service_name in call_direction: + self._state = call_direction.get(self._service_name) diff --git a/requirements_all.txt b/requirements_all.txt index 73ececb0517b6d..26fdfc38fa3565 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1346,7 +1346,7 @@ pynx584==0.4 pynzbgetapi==0.2.0 # homeassistant.components.obihai -pyobihai==1.1.0 +pyobihai==1.1.1 # homeassistant.components.ombi pyombi==0.1.5 From cff7fd0ef3bb314455455b0fca07420a8c9c2fb6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 25 Sep 2019 21:50:14 +0200 Subject: [PATCH 163/296] deCONZ - Increase bridge discovery robustness in config flow (#26911) --- .../components/deconz/config_flow.py | 2 +- tests/components/deconz/test_config_flow.py | 35 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 488d48bb7409d8..66df687047f209 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -90,7 +90,7 @@ async def async_step_user(self, user_input=None): with async_timeout.timeout(10): self.bridges = await async_discovery(session) - except asyncio.TimeoutError: + except (asyncio.TimeoutError, ResponseError): self.bridges = [] if len(self.bridges) == 1: diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 4d8d3a312582a1..d0423c394a6975 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -134,7 +134,9 @@ async def test_user_step_two_bridges_selection(hass, aioclient_mock): assert flow.deconz_config[config_flow.CONF_HOST] == "1.2.3.4" -async def test_user_step_manual_configuration(hass, aioclient_mock): +async def test_user_step_manual_configuration_no_bridges_discovered( + hass, aioclient_mock +): """Test config flow with manual input.""" aioclient_mock.get( pydeconz.utils.URL_DISCOVER, @@ -148,6 +150,7 @@ async def test_user_step_manual_configuration(hass, aioclient_mock): assert result["type"] == "form" assert result["step_id"] == "init" + assert not hass.config_entries.flow._progress[result["flow_id"]].bridges result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -158,6 +161,36 @@ async def test_user_step_manual_configuration(hass, aioclient_mock): assert result["step_id"] == "link" +async def test_user_step_manual_configuration_after_timeout(hass): + """Test config flow with manual input.""" + with patch( + "homeassistant.components.deconz.config_flow.async_discovery", + side_effect=asyncio.TimeoutError, + ): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "init" + assert not hass.config_entries.flow._progress[result["flow_id"]].bridges + + +async def test_user_step_manual_configuration_after_ResponseError(hass): + """Test config flow with manual input.""" + with patch( + "homeassistant.components.deconz.config_flow.async_discovery", + side_effect=config_flow.ResponseError, + ): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "init" + assert not hass.config_entries.flow._progress[result["flow_id"]].bridges + + async def test_link_no_api_key(hass): """Test config flow should abort if no API key was possible to retrieve.""" flow = config_flow.DeconzFlowHandler() From f6995b8d17ca116d994e123b19180af77d9c1eb4 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Wed, 25 Sep 2019 16:38:21 -0400 Subject: [PATCH 164/296] Add config flow to ecobee (#26634) * Add basic config flow * Fix json files * Update __init__.py * Fix json errors * Move constants to const.py * Add ecobee to generated config flows * Update config_flow for updated API * Update manifest to include new dependencies Bump pyecobee, add aiofiles. * Update constants for ecobee * Modify ecobee setup to use config flow * Bump dependency * Update binary_sensor to use config_entry * Update sensor to use config_entry * Update __init__.py * Update weather to use config_entry * Update notify.py * Update ecobee constants * Update climate to use config_entry * Avoid a breaking change on ecobee services * Store api key from old config entry * Allow unloading of config entry * Show user a form before import * Refine import flow * Update strings.json to remove import step Not needed. * Move third party imports to top of module * Remove periods from end of log messages * Make configuration.yaml config optional * Remove unused strings * Reorganize config flow * Remove unneeded requirement * No need to store API key * Update async_unload_entry * Clean up if/else statements * Update requirements_all.txt * Fix config schema * Update __init__.py * Remove check for DATA_ECOBEE_CONFIG * Remove redundant check * Add check for DATA_ECOBEE_CONFIG * Change setup_platform to async * Fix state unknown and imports * Change init step to user * Have import step raise specific exceptions * Rearrange try/except block in import flow * Convert update() and refresh() to coroutines ...and update platforms to use async_update coroutine. * Finish converting init to async * Preliminary tests * Test full implementation * Update test_config_flow.py * Update test_config_flow.py * Add self to codeowners * Update test_config_flow.py * Use MockConfigEntry * Update test_config_flow.py * Update CODEOWNERS * pylint fixes * Register services under ecobee domain Breaking change! * Pylint fixes * Pylint fixes * Pylint fixes * Move service strings to ecobee domain * Fix log message capitalization * Fix import formatting * Update .coveragerc * Add __init__ to coveragerc * Add option flow test * Update .coveragerc * Act on updated options * Revert "Act on updated options" This reverts commit 56b0a859f2e3e80b6f4c77a8f784a2b29ee2cce9. * Remove hold_temp from climate * Remove hold_temp and options from init * Remove options handler from config flow * Remove options strings * Remove options flow test * Remove hold_temp constants * Fix climate tests * Pass api key to user step in import flow * Update test_config_flow.py Ensure that the import step calls the user step with the user's api key as user input if importing from ecobee.conf/validating imported keys fails. --- .coveragerc | 7 +- CODEOWNERS | 1 + .../components/climate/services.yaml | 20 -- .../components/ecobee/.translations/en.json | 23 ++ homeassistant/components/ecobee/__init__.py | 191 ++++++++-------- .../components/ecobee/binary_sensor.py | 38 ++-- homeassistant/components/ecobee/climate.py | 51 ++--- .../components/ecobee/config_flow.py | 120 ++++++++++ homeassistant/components/ecobee/const.py | 12 + homeassistant/components/ecobee/manifest.json | 15 +- homeassistant/components/ecobee/notify.py | 17 +- homeassistant/components/ecobee/sensor.py | 43 ++-- homeassistant/components/ecobee/services.yaml | 19 ++ homeassistant/components/ecobee/strings.json | 23 ++ homeassistant/components/ecobee/weather.py | 46 ++-- homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/ecobee/test_climate.py | 2 +- tests/components/ecobee/test_config_flow.py | 206 ++++++++++++++++++ 21 files changed, 623 insertions(+), 218 deletions(-) create mode 100644 homeassistant/components/ecobee/.translations/en.json create mode 100644 homeassistant/components/ecobee/config_flow.py create mode 100644 homeassistant/components/ecobee/const.py create mode 100644 homeassistant/components/ecobee/strings.json create mode 100644 tests/components/ecobee/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index a4d6d0d201e980..a8932f54a54725 100644 --- a/.coveragerc +++ b/.coveragerc @@ -156,7 +156,12 @@ omit = homeassistant/components/ebox/sensor.py homeassistant/components/ebusd/* homeassistant/components/ecoal_boiler/* - homeassistant/components/ecobee/* + homeassistant/components/ecobee/__init__.py + homeassistant/components/ecobee/binary_sensor.py + homeassistant/components/ecobee/climate.py + homeassistant/components/ecobee/notify.py + homeassistant/components/ecobee/sensor.py + homeassistant/components/ecobee/weather.py homeassistant/components/econet/water_heater.py homeassistant/components/ecovacs/* homeassistant/components/eddystone_temperature/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 7e05cdf0b399e7..cb9180d717daa8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -73,6 +73,7 @@ homeassistant/components/digital_ocean/* @fabaff homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 homeassistant/components/dweet/* @fabaff +homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 4e9a4a3a4f46f5..f10e1b4bd69d71 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -72,26 +72,6 @@ set_swing_mode: swing_mode: description: New value of swing mode. -ecobee_set_fan_min_on_time: - description: Set the minimum fan on time. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'climate.kitchen' - fan_min_on_time: - description: New value of fan min on time. - example: 5 - -ecobee_resume_program: - description: Resume the programmed schedule. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'climate.kitchen' - resume_all: - description: Resume all events and return to the scheduled program. This default to false which removes only the top event. - example: true - mill_set_room_temperature: description: Set Mill room temperatures. fields: diff --git a/homeassistant/components/ecobee/.translations/en.json b/homeassistant/components/ecobee/.translations/en.json new file mode 100644 index 00000000000000..9e7e9fed39659a --- /dev/null +++ b/homeassistant/components/ecobee/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "ecobee", + "step": { + "user": { + "title": "ecobee API key", + "description": "Please enter the API key obtained from ecobee.com.", + "data": {"api_key": "API Key"} + }, + "authorize": { + "title": "Authorize app on ecobee.com", + "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit." + } + }, + "error": { + "pin_request_failed": "Error requesting PIN from ecobee; please verify API key is correct.", + "token_request_failed": "Error requesting tokens from ecobee; please try again." + }, + "abort": { + "one_instance_only": "This integration currently supports only one ecobee instance." + } + } +} diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index cb8b7436b51089..eb65a7ed4269bc 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -1,123 +1,130 @@ -"""Support for Ecobee devices.""" -import logging -import os +"""Support for ecobee.""" +import asyncio from datetime import timedelta - import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery +from pyecobee import Ecobee, ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, ExpiredTokenError + +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_API_KEY +from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle -from homeassistant.util.json import save_json - -_CONFIGURING = {} -_LOGGER = logging.getLogger(__name__) - -CONF_HOLD_TEMP = "hold_temp" -DOMAIN = "ecobee" - -ECOBEE_CONFIG_FILE = "ecobee.conf" +from .const import ( + CONF_REFRESH_TOKEN, + DATA_ECOBEE_CONFIG, + DOMAIN, + ECOBEE_PLATFORMS, + _LOGGER, +) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) -NETWORK = None - CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean, - } - ) - }, - extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.Schema({vol.Optional(CONF_API_KEY): cv.string})}, extra=vol.ALLOW_EXTRA ) -def request_configuration(network, hass, config): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - if "ecobee" in _CONFIGURING: - configurator.notify_errors( - _CONFIGURING["ecobee"], "Failed to register, please try again." - ) - - return - - def ecobee_configuration_callback(callback_data): - """Handle configuration callbacks.""" - network.request_tokens() - network.update() - setup_ecobee(hass, network, config) - - _CONFIGURING["ecobee"] = configurator.request_config( - "Ecobee", - ecobee_configuration_callback, - description=( - "Please authorize this app at https://www.ecobee.com/consumer" - "portal/index.html with pin code: " + network.pin - ), - description_image="/static/images/config_ecobee_thermostat.png", - submit_caption="I have authorized the app.", - ) +async def async_setup(hass, config): + """ + Ecobee uses config flow for configuration. + But, an "ecobee:" entry in configuration.yaml will trigger an import flow + if a config entry doesn't already exist. If ecobee.conf exists, the import + flow will attempt to import it and create a config entry, to assist users + migrating from the old ecobee component. Otherwise, the user will have to + continue setting up the integration via the config flow. + """ + hass.data[DATA_ECOBEE_CONFIG] = config.get(DOMAIN, {}) + + if not hass.config_entries.async_entries(DOMAIN) and hass.data[DATA_ECOBEE_CONFIG]: + # No config entry exists and configuration.yaml config exists, trigger the import flow. + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT} + ) + ) -def setup_ecobee(hass, network, config): - """Set up the Ecobee thermostat.""" - # If ecobee has a PIN then it needs to be configured. - if network.pin is not None: - request_configuration(network, hass, config) - return + return True - if "ecobee" in _CONFIGURING: - configurator = hass.components.configurator - configurator.request_done(_CONFIGURING.pop("ecobee")) - hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP) +async def async_setup_entry(hass, entry): + """Set up ecobee via a config entry.""" + api_key = entry.data[CONF_API_KEY] + refresh_token = entry.data[CONF_REFRESH_TOKEN] - discovery.load_platform(hass, "climate", DOMAIN, {"hold_temp": hold_temp}, config) - discovery.load_platform(hass, "sensor", DOMAIN, {}, config) - discovery.load_platform(hass, "binary_sensor", DOMAIN, {}, config) - discovery.load_platform(hass, "weather", DOMAIN, {}, config) + data = EcobeeData(hass, entry, api_key=api_key, refresh_token=refresh_token) + if not await data.refresh(): + return False -class EcobeeData: - """Get the latest data and update the states.""" + await data.update() - def __init__(self, config_file): - """Init the Ecobee data object.""" - from pyecobee import Ecobee + if data.ecobee.thermostats is None: + _LOGGER.error("No ecobee devices found to set up") + return False - self.ecobee = Ecobee(config_file) + hass.data[DOMAIN] = data - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from pyecobee.""" - self.ecobee.update() - _LOGGER.debug("Ecobee data updated successfully") + for component in ECOBEE_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + return True -def setup(hass, config): - """Set up the Ecobee. - Will automatically load thermostat and sensor components to support - devices discovered on the network. +class EcobeeData: """ - global NETWORK + Handle getting the latest data from ecobee.com so platforms can use it. - if "ecobee" in _CONFIGURING: - return - - # Create ecobee.conf if it doesn't exist - if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)): - jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)} - save_json(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) + Also handle refreshing tokens and updating config entry with refreshed tokens. + """ - NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) + def __init__(self, hass, entry, api_key, refresh_token): + """Initialize the Ecobee data object.""" + self._hass = hass + self._entry = entry + self.ecobee = Ecobee( + config={ECOBEE_API_KEY: api_key, ECOBEE_REFRESH_TOKEN: refresh_token} + ) - setup_ecobee(hass, NETWORK.ecobee, config) + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def update(self): + """Get the latest data from ecobee.com.""" + try: + await self._hass.async_add_executor_job(self.ecobee.update) + _LOGGER.debug("Updating ecobee") + except ExpiredTokenError: + _LOGGER.warning( + "Ecobee update failed; attempting to refresh expired tokens" + ) + await self.refresh() + + async def refresh(self) -> bool: + """Refresh ecobee tokens and update config entry.""" + _LOGGER.debug("Refreshing ecobee tokens and updating config entry") + if await self._hass.async_add_executor_job(self.ecobee.refresh_tokens): + self._hass.config_entries.async_update_entry( + self._entry, + data={ + CONF_API_KEY: self.ecobee.config[ECOBEE_API_KEY], + CONF_REFRESH_TOKEN: self.ecobee.config[ECOBEE_REFRESH_TOKEN], + }, + ) + return True + _LOGGER.error("Error updating ecobee tokens") + return False + + +async def async_unload_entry(hass, config_entry): + """Unload the config entry and platforms.""" + hass.data.pop(DOMAIN) + + tasks = [] + for platform in ECOBEE_PLATFORMS: + tasks.append( + hass.config_entries.async_forward_entry_unload(config_entry, platform) + ) - return True + return all(await asyncio.gather(*tasks)) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index a3cd49ff45871e..8b7b819cfc7141 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -1,15 +1,20 @@ """Support for Ecobee binary sensors.""" -from homeassistant.components import ecobee -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + DEVICE_CLASS_OCCUPANCY, +) -ECOBEE_CONFIG_FILE = "ecobee.conf" +from .const import DOMAIN -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee sensors.""" - if discovery_info is None: - return - data = ecobee.NETWORK +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up ecobee binary sensors.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up ecobee binary (occupancy) sensors.""" + data = hass.data[DOMAIN] dev = list() for index in range(len(data.ecobee.thermostats)): for sensor in data.ecobee.get_remote_sensors(index): @@ -17,21 +22,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if item["type"] != "occupancy": continue - dev.append(EcobeeBinarySensor(sensor["name"], index)) + dev.append(EcobeeBinarySensor(data, sensor["name"], index)) - add_entities(dev, True) + async_add_entities(dev, True) class EcobeeBinarySensor(BinarySensorDevice): """Representation of an Ecobee sensor.""" - def __init__(self, sensor_name, sensor_index): + def __init__(self, data, sensor_name, sensor_index): """Initialize the Ecobee sensor.""" + self.data = data self._name = sensor_name + " Occupancy" self.sensor_name = sensor_name self.index = sensor_index self._state = None - self._device_class = "occupancy" @property def name(self): @@ -46,13 +51,12 @@ def is_on(self): @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" - return self._device_class + return DEVICE_CLASS_OCCUPANCY - def update(self): + async def async_update(self): """Get the latest state of the sensor.""" - data = ecobee.NETWORK - data.update() - for sensor in data.ecobee.get_remote_sensors(self.index): + await self.data.update() + for sensor in self.data.ecobee.get_remote_sensors(self.index): for item in sensor["capability"]: if item["type"] == "occupancy" and self.sensor_name == sensor["name"]: self._state = item["value"] diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 181f1561eba0ca..9eb8e8f26bc6ef 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -1,14 +1,11 @@ """Support for Ecobee Thermostats.""" import collections -import logging from typing import Optional import voluptuous as vol -from homeassistant.components import ecobee from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - DOMAIN, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_AUTO, @@ -38,8 +35,7 @@ ) import homeassistant.helpers.config_validation as cv -_CONFIGURING = {} -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, _LOGGER ATTR_FAN_MIN_ON_TIME = "fan_min_on_time" ATTR_RESUME_ALL = "resume_all" @@ -88,8 +84,8 @@ PRESET_HOLD_INDEFINITE: "indefinite", } -SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time" -SERVICE_RESUME_PROGRAM = "ecobee_resume_program" +SERVICE_SET_FAN_MIN_ON_TIME = "set_fan_min_on_time" +SERVICE_RESUME_PROGRAM = "resume_program" SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema( { @@ -114,20 +110,19 @@ ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee Thermostat Platform.""" - if discovery_info is None: - return - data = ecobee.NETWORK - hold_temp = discovery_info["hold_temp"] - _LOGGER.info( - "Loading ecobee thermostat component with hold_temp set to %s", hold_temp - ) - devices = [ - Thermostat(data, index, hold_temp) - for index in range(len(data.ecobee.thermostats)) - ] - add_entities(devices) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up ecobee thermostat.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the ecobee thermostat.""" + + data = hass.data[DOMAIN] + + devices = [Thermostat(data, index) for index in range(len(data.ecobee.thermostats))] + + async_add_entities(devices, True) def fan_min_on_time_set_service(service): """Set the minimum fan on time on the target thermostats.""" @@ -163,14 +158,14 @@ def resume_program_set_service(service): thermostat.schedule_update_ha_state(True) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service, schema=SET_FAN_MIN_ON_TIME_SCHEMA, ) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service, @@ -181,13 +176,12 @@ def resume_program_set_service(service): class Thermostat(ClimateDevice): """A thermostat class for Ecobee.""" - def __init__(self, data, thermostat_index, hold_temp): + def __init__(self, data, thermostat_index): """Initialize the thermostat.""" self.data = data self.thermostat_index = thermostat_index self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) self._name = self.thermostat["name"] - self.hold_temp = hold_temp self.vacation = None self._operation_list = [] @@ -206,14 +200,13 @@ def __init__(self, data, thermostat_index, hold_temp): self._fan_modes = [FAN_AUTO, FAN_ON] self.update_without_throttle = False - def update(self): + async def async_update(self): """Get the latest state from the thermostat.""" if self.update_without_throttle: - self.data.update(no_throttle=True) + await self.data.update(no_throttle=True) self.update_without_throttle = False else: - self.data.update() - + await self.data.update() self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) @property diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py new file mode 100644 index 00000000000000..f4cd4fc5bf086d --- /dev/null +++ b/homeassistant/components/ecobee/config_flow.py @@ -0,0 +1,120 @@ +"""Config flow to configure ecobee.""" +import voluptuous as vol + +from pyecobee import ( + Ecobee, + ECOBEE_CONFIG_FILENAME, + ECOBEE_API_KEY, + ECOBEE_REFRESH_TOKEN, +) + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistantError +from homeassistant.util.json import load_json + +from .const import CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN, _LOGGER + + +class EcobeeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle an ecobee config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize the ecobee flow.""" + self._ecobee = None + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if self._async_current_entries(): + # Config entry already exists, only one allowed. + return self.async_abort(reason="one_instance_only") + + errors = {} + stored_api_key = self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + + if user_input is not None: + # Use the user-supplied API key to attempt to obtain a PIN from ecobee. + self._ecobee = Ecobee(config={ECOBEE_API_KEY: user_input[CONF_API_KEY]}) + + if await self.hass.async_add_executor_job(self._ecobee.request_pin): + # We have a PIN; move to the next step of the flow. + return await self.async_step_authorize() + errors["base"] = "pin_request_failed" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_API_KEY, default=stored_api_key): str} + ), + errors=errors, + ) + + async def async_step_authorize(self, user_input=None): + """Present the user with the PIN so that the app can be authorized on ecobee.com.""" + errors = {} + + if user_input is not None: + # Attempt to obtain tokens from ecobee and finish the flow. + if await self.hass.async_add_executor_job(self._ecobee.request_tokens): + # Refresh token obtained; create the config entry. + config = { + CONF_API_KEY: self._ecobee.api_key, + CONF_REFRESH_TOKEN: self._ecobee.refresh_token, + } + return self.async_create_entry(title=DOMAIN, data=config) + errors["base"] = "token_request_failed" + + return self.async_show_form( + step_id="authorize", + errors=errors, + description_placeholders={"pin": self._ecobee.pin}, + ) + + async def async_step_import(self, import_data): + """ + Import ecobee config from configuration.yaml. + + Triggered by async_setup only if a config entry doesn't already exist. + If ecobee.conf exists, we will attempt to validate the credentials + and create an entry if valid. Otherwise, we will delegate to the user + step so that the user can continue the config flow. + """ + try: + legacy_config = await self.hass.async_add_executor_job( + load_json, self.hass.config.path(ECOBEE_CONFIG_FILENAME) + ) + config = { + ECOBEE_API_KEY: legacy_config[ECOBEE_API_KEY], + ECOBEE_REFRESH_TOKEN: legacy_config[ECOBEE_REFRESH_TOKEN], + } + except (HomeAssistantError, KeyError): + _LOGGER.debug( + "No valid ecobee.conf configuration found for import, delegating to user step" + ) + return await self.async_step_user( + user_input={ + CONF_API_KEY: self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + } + ) + + ecobee = Ecobee(config=config) + if await self.hass.async_add_executor_job(ecobee.refresh_tokens): + # Credentials found and validated; create the entry. + _LOGGER.debug( + "Valid ecobee configuration found for import, creating config entry" + ) + return self.async_create_entry( + title=DOMAIN, + data={ + CONF_API_KEY: ecobee.api_key, + CONF_REFRESH_TOKEN: ecobee.refresh_token, + }, + ) + return await self.async_step_user( + user_input={ + CONF_API_KEY: self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + } + ) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py new file mode 100644 index 00000000000000..c3a23099b8abcd --- /dev/null +++ b/homeassistant/components/ecobee/const.py @@ -0,0 +1,12 @@ +"""Constants for the ecobee integration.""" +import logging + +_LOGGER = logging.getLogger(__package__) + +DOMAIN = "ecobee" +DATA_ECOBEE_CONFIG = "ecobee_config" + +CONF_INDEX = "index" +CONF_REFRESH_TOKEN = "refresh_token" + +ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 31cca1e676fafd..092594c41fc0d4 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -1,10 +1,9 @@ { - "domain": "ecobee", - "name": "Ecobee", - "documentation": "https://www.home-assistant.io/components/ecobee", - "requirements": [ - "python-ecobee-api==0.0.21" - ], - "dependencies": ["configurator"], - "codeowners": [] + "domain": "ecobee", + "name": "Ecobee", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/ecobee", + "dependencies": [], + "requirements": ["python-ecobee-api==0.1.2"], + "codeowners": ["@marthoc"] } diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index bb6861a1492a97..c7b3f47d29c17f 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -1,15 +1,10 @@ """Support for Ecobee Send Message service.""" -import logging - import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components import ecobee from homeassistant.components.notify import BaseNotificationService, PLATFORM_SCHEMA -_LOGGER = logging.getLogger(__name__) - -CONF_INDEX = "index" +from .const import CONF_INDEX, DOMAIN PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(CONF_INDEX, default=0): cv.positive_int} @@ -18,17 +13,19 @@ def get_service(hass, config, discovery_info=None): """Get the Ecobee notification service.""" + data = hass.data[DOMAIN] index = config.get(CONF_INDEX) - return EcobeeNotificationService(index) + return EcobeeNotificationService(data, index) class EcobeeNotificationService(BaseNotificationService): """Implement the notification service for the Ecobee thermostat.""" - def __init__(self, thermostat_index): + def __init__(self, data, thermostat_index): """Initialize the service.""" + self.data = data self.thermostat_index = thermostat_index def send_message(self, message="", **kwargs): - """Send a message to a command line.""" - ecobee.NETWORK.ecobee.send_message(self.thermostat_index, message) + """Send a message.""" + self.data.ecobee.send_message(self.thermostat_index, message) diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index d21f937dd2062f..e62d68dc9bcfcd 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -1,5 +1,6 @@ """Support for Ecobee sensors.""" -from homeassistant.components import ecobee +from pyecobee.const import ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN + from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -7,7 +8,7 @@ ) from homeassistant.helpers.entity import Entity -ECOBEE_CONFIG_FILE = "ecobee.conf" +from .const import DOMAIN SENSOR_TYPES = { "temperature": ["Temperature", TEMP_FAHRENHEIT], @@ -15,11 +16,14 @@ } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee sensors.""" - if discovery_info is None: - return - data = ecobee.NETWORK +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up ecobee sensors.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up ecobee (temperature and humidity) sensors.""" + data = hass.data[DOMAIN] dev = list() for index in range(len(data.ecobee.thermostats)): for sensor in data.ecobee.get_remote_sensors(index): @@ -27,16 +31,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if item["type"] not in ("temperature", "humidity"): continue - dev.append(EcobeeSensor(sensor["name"], item["type"], index)) + dev.append(EcobeeSensor(data, sensor["name"], item["type"], index)) - add_entities(dev, True) + async_add_entities(dev, True) class EcobeeSensor(Entity): """Representation of an Ecobee sensor.""" - def __init__(self, sensor_name, sensor_type, sensor_index): + def __init__(self, data, sensor_name, sensor_type, sensor_index): """Initialize the sensor.""" + self.data = data self._name = "{} {}".format(sensor_name, SENSOR_TYPES[sensor_type][0]) self.sensor_name = sensor_name self.type = sensor_type @@ -59,6 +64,12 @@ def device_class(self): @property def state(self): """Return the state of the sensor.""" + if self._state in [ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN]: + return None + + if self.type == "temperature": + return float(self._state) / 10 + return self._state @property @@ -66,14 +77,10 @@ def unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement - def update(self): + async def async_update(self): """Get the latest state of the sensor.""" - data = ecobee.NETWORK - data.update() - for sensor in data.ecobee.get_remote_sensors(self.index): + await self.data.update() + for sensor in self.data.ecobee.get_remote_sensors(self.index): for item in sensor["capability"]: if item["type"] == self.type and self.sensor_name == sensor["name"]: - if self.type == "temperature" and item["value"] != "unknown": - self._state = float(item["value"]) / 10 - else: - self._state = item["value"] + self._state = item["value"] diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml index e69de29bb2d1d6..87eefed9eaaa43 100644 --- a/homeassistant/components/ecobee/services.yaml +++ b/homeassistant/components/ecobee/services.yaml @@ -0,0 +1,19 @@ +resume_program: + description: Resume the programmed schedule. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' + resume_all: + description: Resume all events and return to the scheduled program. This default to false which removes only the top event. + example: true + +set_fan_min_on_time: + description: Set the minimum fan on time. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' + fan_min_on_time: + description: New value of fan min on time. + example: 5 diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json new file mode 100644 index 00000000000000..9e7e9fed39659a --- /dev/null +++ b/homeassistant/components/ecobee/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "ecobee", + "step": { + "user": { + "title": "ecobee API key", + "description": "Please enter the API key obtained from ecobee.com.", + "data": {"api_key": "API Key"} + }, + "authorize": { + "title": "Authorize app on ecobee.com", + "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit." + } + }, + "error": { + "pin_request_failed": "Error requesting PIN from ecobee; please verify API key is correct.", + "token_request_failed": "Error requesting tokens from ecobee; please try again." + }, + "abort": { + "one_instance_only": "This integration currently supports only one ecobee instance." + } + } +} diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index b09e06bd822dd4..dd3112b636e0c3 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -1,7 +1,8 @@ """Support for displaying weather info from Ecobee API.""" from datetime import datetime -from homeassistant.components import ecobee +from pyecobee.const import ECOBEE_STATE_UNKNOWN + from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, @@ -12,33 +13,37 @@ ) from homeassistant.const import TEMP_FAHRENHEIT +from .const import DOMAIN + ATTR_FORECAST_TEMP_HIGH = "temphigh" ATTR_FORECAST_PRESSURE = "pressure" ATTR_FORECAST_VISIBILITY = "visibility" ATTR_FORECAST_HUMIDITY = "humidity" -MISSING_DATA = -5002 + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up the ecobee weather platform.""" + pass -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee weather platform.""" - if discovery_info is None: - return +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the ecobee weather platform.""" + data = hass.data[DOMAIN] dev = list() - data = ecobee.NETWORK for index in range(len(data.ecobee.thermostats)): thermostat = data.ecobee.get_thermostat(index) if "weather" in thermostat: - dev.append(EcobeeWeather(thermostat["name"], index)) + dev.append(EcobeeWeather(data, thermostat["name"], index)) - add_entities(dev, True) + async_add_entities(dev, True) class EcobeeWeather(WeatherEntity): """Representation of Ecobee weather data.""" - def __init__(self, name, index): + def __init__(self, data, name, index): """Initialize the Ecobee weather platform.""" + self.data = data self._name = name self._index = index self.weather = None @@ -140,26 +145,25 @@ def forecast(self): ATTR_FORECAST_CONDITION: day["condition"], ATTR_FORECAST_TEMP: float(day["tempHigh"]) / 10, } - if day["tempHigh"] == MISSING_DATA: + if day["tempHigh"] == ECOBEE_STATE_UNKNOWN: break - if day["tempLow"] != MISSING_DATA: + if day["tempLow"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_TEMP_LOW] = float(day["tempLow"]) / 10 - if day["pressure"] != MISSING_DATA: + if day["pressure"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_PRESSURE] = int(day["pressure"]) - if day["windSpeed"] != MISSING_DATA: + if day["windSpeed"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_WIND_SPEED] = int(day["windSpeed"]) - if day["visibility"] != MISSING_DATA: + if day["visibility"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_WIND_SPEED] = int(day["visibility"]) - if day["relativeHumidity"] != MISSING_DATA: + if day["relativeHumidity"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_HUMIDITY] = int(day["relativeHumidity"]) forecasts.append(forecast) return forecasts except (ValueError, IndexError, KeyError): return None - def update(self): - """Get the latest state of the sensor.""" - data = ecobee.NETWORK - data.update() - thermostat = data.ecobee.get_thermostat(self._index) + async def async_update(self): + """Get the latest weather data.""" + await self.data.update() + thermostat = self.data.ecobee.get_thermostat(self._index) self.weather = thermostat.get("weather", None) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9a534c01bbf2c6..b6865f9e86a16e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -15,6 +15,7 @@ "daikin", "deconz", "dialogflow", + "ecobee", "emulated_roku", "esphome", "geofency", diff --git a/requirements_all.txt b/requirements_all.txt index 26fdfc38fa3565..5ca7a7c8160bd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1483,7 +1483,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.0.21 +python-ecobee-api==0.1.2 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f542147363915..d6c0d8dbbb2905 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -355,6 +355,9 @@ pysonos==0.0.23 # homeassistant.components.spc pyspcwebgw==0.4.0 +# homeassistant.components.ecobee +python-ecobee-api==0.1.2 + # homeassistant.components.darksky python-forecastio==1.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 649c48e1b7d282..fcb265bbc97b6b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -146,6 +146,7 @@ "pysonos", "pyspcwebgw", "python_awair", + "python-ecobee-api", "python-forecastio", "python-izone", "python-nest", diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index d6c40ddf9ab389..90a9a6417765a7 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -54,7 +54,7 @@ def setUp(self): self.data = mock.Mock() self.data.ecobee.get_thermostat.return_value = self.ecobee - self.thermostat = ecobee.Thermostat(self.data, 1, False) + self.thermostat = ecobee.Thermostat(self.data, 1) def test_name(self): """Test name property.""" diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py new file mode 100644 index 00000000000000..7b4d1f96a3761c --- /dev/null +++ b/tests/components/ecobee/test_config_flow.py @@ -0,0 +1,206 @@ +"""Tests for the ecobee config flow.""" +from unittest.mock import patch + +from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN + +from homeassistant import data_entry_flow +from homeassistant.components.ecobee import config_flow +from homeassistant.components.ecobee.const import ( + CONF_REFRESH_TOKEN, + DATA_ECOBEE_CONFIG, + DOMAIN, +) +from homeassistant.const import CONF_API_KEY +from tests.common import MockConfigEntry, mock_coro + + +async def test_abort_if_already_setup(hass): + """Test we abort if ecobee is already setup.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await flow.async_step_user() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "one_instance_only" + + +async def test_user_step_without_user_input(hass): + """Test expected result if user step is called.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {} + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_pin_request_succeeds(hass): + """Test expected result if pin request succeeds.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {} + + with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: + mock_ecobee = MockEcobee.return_value + mock_ecobee.request_pin.return_value = True + mock_ecobee.pin = "test-pin" + + result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "authorize" + assert result["description_placeholders"] == {"pin": "test-pin"} + + +async def test_pin_request_fails(hass): + """Test expected result if pin request fails.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {} + + with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: + mock_ecobee = MockEcobee.return_value + mock_ecobee.request_pin.return_value = False + + result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "pin_request_failed" + + +async def test_token_request_succeeds(hass): + """Test expected result if token request succeeds.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {} + + with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: + mock_ecobee = MockEcobee.return_value + mock_ecobee.request_tokens.return_value = True + mock_ecobee.api_key = "test-api-key" + mock_ecobee.refresh_token = "test-token" + flow._ecobee = mock_ecobee + + result = await flow.async_step_authorize(user_input={}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DOMAIN + assert result["data"] == { + CONF_API_KEY: "test-api-key", + CONF_REFRESH_TOKEN: "test-token", + } + + +async def test_token_request_fails(hass): + """Test expected result if token request fails.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {} + + with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: + mock_ecobee = MockEcobee.return_value + mock_ecobee.request_tokens.return_value = False + mock_ecobee.pin = "test-pin" + flow._ecobee = mock_ecobee + + result = await flow.async_step_authorize(user_input={}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "authorize" + assert result["errors"]["base"] == "token_request_failed" + assert result["description_placeholders"] == {"pin": "test-pin"} + + +async def test_import_flow_triggered_but_no_ecobee_conf(hass): + """Test expected result if import flow triggers but ecobee.conf doesn't exist.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {} + + result = await flow.async_step_import(import_data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_tokens( + hass +): + """Test expected result if import flow triggers and ecobee.conf exists with valid tokens.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + + MOCK_ECOBEE_CONF = {ECOBEE_API_KEY: None, ECOBEE_REFRESH_TOKEN: None} + + with patch( + "homeassistant.components.ecobee.config_flow.load_json", + return_value=MOCK_ECOBEE_CONF, + ), patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: + mock_ecobee = MockEcobee.return_value + mock_ecobee.refresh_tokens.return_value = True + mock_ecobee.api_key = "test-api-key" + mock_ecobee.refresh_token = "test-token" + + result = await flow.async_step_import(import_data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DOMAIN + assert result["data"] == { + CONF_API_KEY: "test-api-key", + CONF_REFRESH_TOKEN: "test-token", + } + + +async def test_import_flow_triggered_with_ecobee_conf_and_invalid_data(hass): + """Test expected result if import flow triggers and ecobee.conf exists with invalid data.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {CONF_API_KEY: "test-api-key"} + + MOCK_ECOBEE_CONF = {} + + with patch( + "homeassistant.components.ecobee.config_flow.load_json", + return_value=MOCK_ECOBEE_CONF, + ), patch.object( + flow, "async_step_user", return_value=mock_coro() + ) as mock_async_step_user: + + await flow.async_step_import(import_data=None) + + mock_async_step_user.assert_called_once_with( + user_input={CONF_API_KEY: "test-api-key"} + ) + + +async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_stale_tokens( + hass +): + """Test expected result if import flow triggers and ecobee.conf exists with stale tokens.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {CONF_API_KEY: "test-api-key"} + + MOCK_ECOBEE_CONF = {ECOBEE_API_KEY: None, ECOBEE_REFRESH_TOKEN: None} + + with patch( + "homeassistant.components.ecobee.config_flow.load_json", + return_value=MOCK_ECOBEE_CONF, + ), patch( + "homeassistant.components.ecobee.config_flow.Ecobee" + ) as MockEcobee, patch.object( + flow, "async_step_user", return_value=mock_coro() + ) as mock_async_step_user: + mock_ecobee = MockEcobee.return_value + mock_ecobee.refresh_tokens.return_value = False + + await flow.async_step_import(import_data=None) + + mock_async_step_user.assert_called_once_with( + user_input={CONF_API_KEY: "test-api-key"} + ) From b75639d9d13ed2032df8aaa5c9ae34e0e5d1d6b6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 25 Sep 2019 23:00:18 +0200 Subject: [PATCH 165/296] Remove lamps and groups from ha when removed from Hue (#26881) * Remove light when removed from hue * add remove_config_entry_id * Review + bump aiohue * lint * Add tests --- homeassistant/components/hue/light.py | 43 +++++++++++++--- homeassistant/components/hue/manifest.json | 2 +- homeassistant/helpers/device_registry.py | 6 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/test_light.py | 56 +++++++++++++++++++++ tests/helpers/test_device_registry.py | 58 ++++++++++++++++++++++ 7 files changed, 159 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 96c749a3413e18..5a3379f71ce880 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -8,6 +8,9 @@ import aiohue import async_timeout +from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg + from homeassistant.components import hue from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -147,6 +150,7 @@ async def update_bridge(): tasks.append( async_update_items( hass, + config_entry, bridge, async_add_entities, request_update, @@ -160,6 +164,7 @@ async def update_bridge(): tasks.append( async_update_items( hass, + config_entry, bridge, async_add_entities, request_update, @@ -176,6 +181,7 @@ async def update_bridge(): async def async_update_items( hass, + config_entry, bridge, async_add_entities, request_bridge_update, @@ -204,9 +210,9 @@ async def async_update_items( _LOGGER.error("Unable to reach bridge %s (%s)", bridge.host, err) bridge.available = False - for light_id, light in current.items(): - if light_id not in progress_waiting: - light.async_schedule_update_ha_state() + for item_id, item in current.items(): + if item_id not in progress_waiting: + item.async_schedule_update_ha_state() return @@ -219,7 +225,8 @@ async def async_update_items( _LOGGER.info("Reconnected to bridge %s", bridge.host) bridge.available = True - new_lights = [] + new_items = [] + removed_items = [] for item_id in api: if item_id not in current: @@ -227,12 +234,34 @@ async def async_update_items( api[item_id], request_bridge_update, bridge, is_group ) - new_lights.append(current[item_id]) + new_items.append(current[item_id]) elif item_id not in progress_waiting: current[item_id].async_schedule_update_ha_state() - if new_lights: - async_add_entities(new_lights) + for item_id in current: + if item_id in api: + continue + + # Device is removed from Hue, so we remove it from Home Assistant + entity = current[item_id] + removed_items.append(item_id) + await entity.async_remove() + ent_registry = await get_ent_reg(hass) + if entity.entity_id in ent_registry.entities: + ent_registry.async_remove(entity.entity_id) + dev_registry = await get_dev_reg(hass) + device = dev_registry.async_get_device( + identifiers={(hue.DOMAIN, entity.unique_id)}, connections=set() + ) + dev_registry.async_update_device( + device.id, remove_config_entry_id=config_entry.entry_id + ) + + if new_items: + async_add_entities(new_items) + + for item_id in removed_items: + del current[item_id] class HueLight(Light): diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index c0c7c462f905a7..cb37dd3036f245 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/hue", "requirements": [ - "aiohue==1.9.1" + "aiohue==1.9.2" ], "ssdp": { "manufacturer": [ diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 19b4a1333b6861..456678edac779a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -157,6 +157,7 @@ def async_update_device( name_by_user=_UNDEF, new_identifiers=_UNDEF, via_device_id=_UNDEF, + remove_config_entry_id=_UNDEF, ): """Update properties of a device.""" return self._async_update_device( @@ -166,6 +167,7 @@ def async_update_device( name_by_user=name_by_user, new_identifiers=new_identifiers, via_device_id=via_device_id, + remove_config_entry_id=remove_config_entry_id, ) @callback @@ -203,6 +205,10 @@ def _async_update_device( remove_config_entry_id is not _UNDEF and remove_config_entry_id in config_entries ): + if config_entries == {remove_config_entry_id}: + self.async_remove_device(device_id) + return + config_entries = config_entries - {remove_config_entry_id} if config_entries is not old.config_entries: diff --git a/requirements_all.txt b/requirements_all.txt index 5ca7a7c8160bd1..6481137f3e6a51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -152,7 +152,7 @@ aioharmony==0.1.13 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.1 +aiohue==1.9.2 # homeassistant.components.imap aioimaplib==0.7.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6c0d8dbbb2905..d42120c339ea27 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -62,7 +62,7 @@ aioesphomeapi==2.2.0 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.1 +aiohue==1.9.2 # homeassistant.components.notion aionotion==1.1.0 diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 1c891b9c84012b..582cc185bc81d6 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -420,6 +420,62 @@ async def test_new_light_discovered(hass, mock_bridge): assert light.state == "off" +async def test_group_removed(hass, mock_bridge): + """Test if 2nd update has removed group.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append({"1": GROUP_RESPONSE["1"]}) + + # Calling a service will trigger the updates to run + await hass.services.async_call( + "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True + ) + + # 2x group update, 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 5 + assert len(hass.states.async_all()) == 2 + + group = hass.states.get("light.group_1") + assert group is not None + + removed_group = hass.states.get("light.group_2") + assert removed_group is None + + +async def test_light_removed(hass, mock_bridge): + """Test if 2nd update has removed light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 3 + + mock_bridge.mock_light_responses.clear() + mock_bridge.mock_light_responses.append({"1": LIGHT_RESPONSE.get("1")}) + + # Calling a service will trigger the updates to run + await hass.services.async_call( + "light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True + ) + + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 + + light = hass.states.get("light.hue_lamp_1") + assert light is not None + + removed_light = hass.states.get("light.hue_lamp_2") + assert removed_light is None + + async def test_other_group_update(hass, mock_bridge): """Test changing one group that will impact the state of other light.""" mock_bridge.allow_groups = True diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index b854b62853c1d0..1b146e9cb129c0 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -409,6 +409,64 @@ async def test_update(registry): assert updated_entry.via_device_id == "98765B" +async def test_update_remove_config_entries(hass, registry, update_events): + """Make sure we do not get duplicate entries.""" + entry = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry2 = registry.async_get_or_create( + config_entry_id="456", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry3 = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, + identifiers={("bridgeid", "4567")}, + manufacturer="manufacturer", + model="model", + ) + + assert len(registry.devices) == 2 + assert entry.id == entry2.id + assert entry.id != entry3.id + assert entry2.config_entries == {"123", "456"} + + updated_entry = registry.async_update_device( + entry2.id, remove_config_entry_id="123" + ) + removed_entry = registry.async_update_device( + entry3.id, remove_config_entry_id="123" + ) + + assert updated_entry.config_entries == {"456"} + assert removed_entry is None + + removed_entry = registry.async_get_device({("bridgeid", "4567")}, set()) + + assert removed_entry is None + + await hass.async_block_till_done() + + assert len(update_events) == 5 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert update_events[1]["action"] == "update" + assert update_events[1]["device_id"] == entry2.id + assert update_events[2]["action"] == "create" + assert update_events[2]["device_id"] == entry3.id + assert update_events[3]["action"] == "update" + assert update_events[3]["device_id"] == entry.id + assert update_events[4]["action"] == "remove" + assert update_events[4]["device_id"] == entry3.id + + async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" with asynctest.patch( From d1b4bd22ce9d499a4fdd950694e444fdddf391d0 Mon Sep 17 00:00:00 2001 From: petewill Date: Wed, 25 Sep 2019 15:20:02 -0600 Subject: [PATCH 166/296] Add MySensors ACK (#26894) * Add MySensors ACK The addition of the ACK will ask sensors to respond to commands sent to them which will update the MySensors device in Home Assistant. Currently, if a default MySensors sketch is used the device will update but Home Assistant does not reflect the update and custom code has to be added to tell Home Assistant the command was received. With the ACK set to 1 in the message all this is taken care of by MySensors. * Run black --- homeassistant/components/mysensors/climate.py | 12 +++++++++--- homeassistant/components/mysensors/cover.py | 14 ++++++++++---- homeassistant/components/mysensors/light.py | 10 ++++++---- homeassistant/components/mysensors/switch.py | 16 ++++++++++++---- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index f534f96b7804ac..dc053e60de15e7 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -156,7 +156,9 @@ async def async_set_temperature(self, **kwargs): (set_req.V_HVAC_SETPOINT_COOL, high), ] for value_type, value in updates: - self.gateway.set_child_value(self.node_id, self.child_id, value_type, value) + self.gateway.set_child_value( + self.node_id, self.child_id, value_type, value, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[value_type] = value @@ -166,7 +168,7 @@ async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode + self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode, ack=1 ) if self.gateway.optimistic: # Optimistically assume that device has changed state @@ -176,7 +178,11 @@ async def async_set_fan_mode(self, fan_mode): async def async_set_hvac_mode(self, hvac_mode): """Set new target temperature.""" self.gateway.set_child_value( - self.node_id, self.child_id, self.value_type, DICT_HA_TO_MYS[hvac_mode] + self.node_id, + self.child_id, + self.value_type, + DICT_HA_TO_MYS[hvac_mode], + ack=1, ) if self.gateway.optimistic: # Optimistically assume that device has changed state diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index bb764e375f36c1..6c02e430ba460b 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -43,7 +43,9 @@ def current_cover_position(self): async def async_open_cover(self, **kwargs): """Move the cover up.""" set_req = self.gateway.const.SetReq - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_UP, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_UP, 1, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that cover has changed state. if set_req.V_DIMMER in self._values: @@ -55,7 +57,9 @@ async def async_open_cover(self, **kwargs): async def async_close_cover(self, **kwargs): """Move the cover down.""" set_req = self.gateway.const.SetReq - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_DOWN, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_DOWN, 1, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that cover has changed state. if set_req.V_DIMMER in self._values: @@ -69,7 +73,7 @@ async def async_set_cover_position(self, **kwargs): position = kwargs.get(ATTR_POSITION) set_req = self.gateway.const.SetReq self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_DIMMER, position + self.node_id, self.child_id, set_req.V_DIMMER, position, ack=1 ) if self.gateway.optimistic: # Optimistically assume that cover has changed state. @@ -79,4 +83,6 @@ async def async_set_cover_position(self, **kwargs): async def async_stop_cover(self, **kwargs): """Stop the device.""" set_req = self.gateway.const.SetReq - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_STOP, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_STOP, 1, ack=1 + ) diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 3936aefab0c033..8f0d0906311c68 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -75,7 +75,9 @@ def _turn_on_light(self): if self._state: return - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_LIGHT, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1 + ) if self.gateway.optimistic: # optimistically assume that light has changed state @@ -96,7 +98,7 @@ def _turn_on_dimmer(self, **kwargs): brightness = kwargs[ATTR_BRIGHTNESS] percent = round(100 * brightness / 255) self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_DIMMER, percent + self.node_id, self.child_id, set_req.V_DIMMER, percent, ack=1 ) if self.gateway.optimistic: @@ -129,7 +131,7 @@ def _turn_on_rgb_and_w(self, hex_template, **kwargs): if len(rgb) > 3: white = rgb.pop() self.gateway.set_child_value( - self.node_id, self.child_id, self.value_type, hex_color + self.node_id, self.child_id, self.value_type, hex_color, ack=1 ) if self.gateway.optimistic: @@ -141,7 +143,7 @@ def _turn_on_rgb_and_w(self, hex_template, **kwargs): async def async_turn_off(self, **kwargs): """Turn the device off.""" value_type = self.gateway.const.SetReq.V_LIGHT - self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0) + self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0, ack=1) if self.gateway.optimistic: # optimistically assume that light has changed state self._state = False diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index df429460541dce..c624aaafa343f4 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -92,7 +92,9 @@ def is_on(self): async def async_turn_on(self, **kwargs): """Turn the switch on.""" - self.gateway.set_child_value(self.node_id, self.child_id, self.value_type, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, 1, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[self.value_type] = STATE_ON @@ -100,7 +102,9 @@ async def async_turn_on(self, **kwargs): async def async_turn_off(self, **kwargs): """Turn the switch off.""" - self.gateway.set_child_value(self.node_id, self.child_id, self.value_type, 0) + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, 0, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[self.value_type] = STATE_OFF @@ -129,7 +133,9 @@ async def async_turn_on(self, **kwargs): self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, self._ir_code ) - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_LIGHT, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[self.value_type] = self._ir_code @@ -141,7 +147,9 @@ async def async_turn_on(self, **kwargs): async def async_turn_off(self, **kwargs): """Turn the IR switch off.""" set_req = self.gateway.const.SetReq - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_LIGHT, 0) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[set_req.V_LIGHT] = STATE_OFF From ba92d781b4ed074492e1ece96163921b92b1839f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 26 Sep 2019 00:32:13 +0000 Subject: [PATCH 167/296] [ci skip] Translation update --- .../arcam_fmj/.translations/bg.json | 5 + .../binary_sensor/.translations/bg.json | 92 +++++++++++++++++++ .../binary_sensor/.translations/da.json | 65 +++++++++++++ .../binary_sensor/.translations/lb.json | 92 +++++++++++++++++++ .../binary_sensor/.translations/pl.json | 21 +++++ .../cert_expiry/.translations/bg.json | 24 +++++ .../components/deconz/.translations/bg.json | 18 ++++ .../components/deconz/.translations/lb.json | 4 +- .../components/ecobee/.translations/bg.json | 25 +++++ .../components/ecobee/.translations/en.json | 32 ++++--- .../geonetnz_quakes/.translations/bg.json | 17 ++++ .../components/izone/.translations/bg.json | 15 +++ .../components/life360/.translations/bg.json | 1 + .../components/light/.translations/bg.json | 4 + .../components/light/.translations/pl.json | 2 +- .../components/plex/.translations/bg.json | 45 +++++++++ .../components/plex/.translations/fr.json | 5 + .../components/plex/.translations/lb.json | 12 +++ .../solaredge/.translations/bg.json | 3 +- .../components/switch/.translations/bg.json | 2 + .../components/switch/.translations/pl.json | 4 +- .../components/toon/.translations/bg.json | 1 + .../components/traccar/.translations/bg.json | 18 ++++ .../twentemilieu/.translations/bg.json | 23 +++++ .../components/unifi/.translations/bg.json | 12 +++ .../components/velbus/.translations/bg.json | 21 +++++ .../components/vesync/.translations/bg.json | 20 ++++ .../components/withings/.translations/bg.json | 20 ++++ .../components/zha/.translations/bg.json | 43 +++++++++ .../components/zha/.translations/da.json | 17 ++++ .../components/zha/.translations/lb.json | 43 +++++++++ .../components/zha/.translations/nl.json | 26 ++++++ .../components/zha/.translations/no.json | 24 ++++- 33 files changed, 734 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/bg.json create mode 100644 homeassistant/components/binary_sensor/.translations/bg.json create mode 100644 homeassistant/components/binary_sensor/.translations/da.json create mode 100644 homeassistant/components/binary_sensor/.translations/lb.json create mode 100644 homeassistant/components/binary_sensor/.translations/pl.json create mode 100644 homeassistant/components/cert_expiry/.translations/bg.json create mode 100644 homeassistant/components/ecobee/.translations/bg.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/bg.json create mode 100644 homeassistant/components/izone/.translations/bg.json create mode 100644 homeassistant/components/plex/.translations/bg.json create mode 100644 homeassistant/components/traccar/.translations/bg.json create mode 100644 homeassistant/components/twentemilieu/.translations/bg.json create mode 100644 homeassistant/components/velbus/.translations/bg.json create mode 100644 homeassistant/components/vesync/.translations/bg.json create mode 100644 homeassistant/components/withings/.translations/bg.json diff --git a/homeassistant/components/arcam_fmj/.translations/bg.json b/homeassistant/components/arcam_fmj/.translations/bg.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/bg.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/bg.json b/homeassistant/components/binary_sensor/.translations/bg.json new file mode 100644 index 00000000000000..9b9741b960196c --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/bg.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0435 \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430", + "is_cold": "{entity_name} \u0435 \u0441\u0442\u0443\u0434\u0435\u043d", + "is_connected": "{entity_name} \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d", + "is_gas": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "is_hot": "{entity_name} \u0435 \u0433\u043e\u0440\u0435\u0449", + "is_light": "{entity_name} \u0437\u0430\u0441\u0438\u0447\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "is_locked": "{entity_name} \u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "is_moist": "{entity_name} \u0435 \u0432\u043b\u0430\u0436\u0435\u043d", + "is_motion": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_moving": "{entity_name} \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", + "is_no_gas": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "is_no_light": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "is_no_motion": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_no_problem": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "is_no_sound": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0437\u0432\u0443\u043a", + "is_no_vibration": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438", + "is_not_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0435 \u0437\u0430\u0440\u0435\u0434\u0435\u043d\u0430", + "is_not_cold": "{entity_name} \u043d\u0435 \u0435 \u0441\u0442\u0443\u0434\u0435\u043d", + "is_not_connected": "{entity_name} \u0435 \u0440\u0430\u0437\u043a\u0430\u0447\u0435\u043d", + "is_not_hot": "{entity_name} \u043d\u0435 \u0435 \u0433\u043e\u0440\u0435\u0449", + "is_not_locked": "{entity_name} \u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d", + "is_not_moist": "{entity_name} \u0435 \u0441\u0443\u0445", + "is_not_moving": "{entity_name} \u043d\u0435 \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", + "is_not_occupied": "{entity_name} \u043d\u0435 \u0435 \u0437\u0430\u0435\u0442", + "is_not_open": "{entity_name} \u0435 \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "is_not_plugged_in": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_not_powered": "{entity_name} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "is_not_present": "{entity_name} \u043d\u0435 \u0435 \u043d\u0430\u043b\u0438\u0446\u0435", + "is_not_unsafe": "{entity_name} \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "is_occupied": "{entity_name} \u0435 \u0437\u0430\u0435\u0442", + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "is_open": "{entity_name} \u0435 \u043e\u0442\u0432\u043e\u0440\u0435\u043d", + "is_plugged_in": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "is_powered": "{entity_name} \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "is_present": "{entity_name} \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", + "is_problem": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "is_smoke": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "is_sound": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0437\u0432\u0443\u043a", + "is_unsafe": "{entity_name} \u043d\u0435 \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "is_vibration": "{entity_name} \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" + }, + "trigger_type": { + "bat_low": "{entity_name} \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f", + "closed": "{entity_name} \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "cold": "{entity_name} \u0441\u0435 \u0438\u0437\u0441\u0442\u0443\u0434\u0438", + "connected": "{entity_name} \u0441\u0432\u044a\u0440\u0437\u0430\u043d", + "gas": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "hot": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", + "light": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "locked": "{entity_name} \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "moist\u00a7": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0432\u043b\u0430\u0436\u0435\u043d", + "motion": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "moving": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "no_gas": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "no_light": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "no_motion": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "no_problem": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "no_smoke": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "no_sound": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0437\u0432\u0443\u043a", + "no_vibration": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438", + "not_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u043d\u0435 \u0435 \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430", + "not_cold": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", + "not_connected": "{entity_name} \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "not_hot": "{entity_name} \u043e\u0445\u043b\u0430\u0434\u043d\u044f", + "not_locked": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d", + "not_moist": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0441\u0443\u0445", + "not_moving": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", + "not_occupied": "{entity_name} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0435 \u0437\u0430\u0435\u0442", + "not_plugged_in": "{entity_name} \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "not_powered": "{entity_name} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "not_present": "{entity_name} \u043d\u0435 \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", + "not_unsafe": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "occupied": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0437\u0430\u0435\u0442", + "opened": "{entity_name} \u0441\u0435 \u043e\u0442\u0432\u043e\u0440\u0438", + "plugged_in": "{entity_name} \u0441\u0435 \u0432\u043a\u043b\u044e\u0447\u0438", + "powered": "{entity_name} \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "present": "{entity_name} \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", + "problem": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "smoke": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "sound": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0437\u0432\u0443\u043a", + "turned_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "turned_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "unsafe": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u043e\u043f\u0430\u0441\u0435\u043d", + "vibration": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/da.json b/homeassistant/components/binary_sensor/.translations/da.json new file mode 100644 index 00000000000000..56822c2365ce71 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/da.json @@ -0,0 +1,65 @@ +{ + "device_automation": { + "condition_type": { + "is_cold": "{entity_name} er kold", + "is_connected": "{entity_name} er tilsluttet", + "is_gas": "{entity_name} registrerer gas", + "is_hot": "{entity_name} er varm", + "is_light": "{entity_name} registrerer lys", + "is_locked": "{entity_name} er l\u00e5st", + "is_moist": "{entity_name} er fugtig", + "is_motion": "{entity_name} registrerer bev\u00e6gelse", + "is_moving": "{entity_name} bev\u00e6ger sig", + "is_no_gas": "{entity_name} registrerer ikke gas", + "is_no_light": "{entity_name} registrerer ikke lys", + "is_no_motion": "{entity_name} registrerer ikke bev\u00e6gelse", + "is_no_problem": "{entity_name} registrerer ikke noget problem", + "is_no_smoke": "{entity_name} registrerer ikke r\u00f8g", + "is_no_sound": "{entity_name} registrerer ikke lyd", + "is_no_vibration": "{entity_name} registrerer ikke vibration", + "is_not_cold": "{entity_name} er ikke kold", + "is_not_connected": "{entity_name} er afbrudt", + "is_not_hot": "{entity_name} er ikke varm", + "is_not_locked": "{entity_name} er l\u00e5st op", + "is_not_moist": "{entity_name} er t\u00f8r", + "is_not_moving": "{entity_name} bev\u00e6ger sig ikke", + "is_not_occupied": "{entity_name} er ikke optaget", + "is_not_open": "{entity_name} er lukket", + "is_not_present": "{entity_name} er ikke til stede", + "is_not_unsafe": "{entity_name} er sikker", + "is_occupied": "{entity_name} er optaget", + "is_open": "{entity_name} er \u00e5ben", + "is_problem": "{entity_name} registrerer problem", + "is_smoke": "{entity_name} registrerer r\u00f8g", + "is_sound": "{entity_name} registrerer lyd", + "is_unsafe": "{entity_name} er usikker", + "is_vibration": "{entity_name} registrerer vibration" + }, + "trigger_type": { + "closed": "{entity_name} lukket", + "cold": "{entity_name} blev kold", + "connected": "{entity_name} tilsluttet", + "moist\u00a7": "{entity_name} blev fugtig", + "motion": "{entity_name} begyndte at registrere bev\u00e6gelse", + "moving": "{entity_name} begyndte at bev\u00e6ge sig", + "no_gas": "{entity_name} stoppede med at registrere gas", + "no_light": "{entity_name} stoppede med at registrere lys", + "no_motion": "{entity_name} stoppede med at registrere bev\u00e6gelse", + "no_problem": "{entity_name} stoppede med at registrere problem", + "no_smoke": "{entity_name} stoppede med at registrere r\u00f8g", + "no_sound": "{entity_name} stoppede med at registrere lyd", + "no_vibration": "{entity_name} stoppede med at registrere vibration", + "not_connected": "{entity_name} afbrudt", + "not_hot": "{entity_name} blev ikke varm", + "not_locked": "{entity_name} l\u00e5st op", + "not_moist": "{entity_name} blev t\u00f8r", + "not_present": "{entity_name} ikke til stede", + "not_unsafe": "{entity_name} blev sikker", + "occupied": "{entity_name} blev optaget", + "present": "{entity_name} til stede", + "problem": "{entity_name} begyndte at registrere problem", + "smoke": "{entity_name} begyndte at registrere r\u00f8g", + "sound": "{entity_name} begyndte at registrere lyd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/lb.json b/homeassistant/components/binary_sensor/.translations/lb.json new file mode 100644 index 00000000000000..0b10e1f51a52c5 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/lb.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} Batterie ass niddereg", + "is_cold": "{entity_name} ass kal", + "is_connected": "{entity_name} ass verbonnen", + "is_gas": "{entity_name} entdeckt Gas", + "is_hot": "{entity_name} ass waarm", + "is_light": "{entity_name} entdeckt Luucht", + "is_locked": "{entity_name} ass gespaart", + "is_moist": "{entity_name} ass fiicht", + "is_motion": "{entity_name} entdeckt Beweegung", + "is_moving": "{entity_name} beweegt sech", + "is_no_gas": "{entity_name} entdeckt kee Gas", + "is_no_light": "{entity_name} entdeckt keng Luucht", + "is_no_motion": "{entity_name} entdeckt keng Beweegung", + "is_no_problem": "{entity_name} entdeckt keng Problemer", + "is_no_smoke": "{entity_name} entdeckt keen Damp", + "is_no_sound": "{entity_name} entdeckt keen Toun", + "is_no_vibration": "{entity_name} entdeckt keng Vibratiounen", + "is_not_bat_low": "{entity_name} Batterie ass normal", + "is_not_cold": "{entity_name} ass net kal", + "is_not_connected": "{entity_name} ass d\u00e9connect\u00e9iert", + "is_not_hot": "{entity_name} ass net waarm", + "is_not_locked": "{entity_name} ass entspaart", + "is_not_moist": "{entity_name} ass dr\u00e9chen", + "is_not_moving": "{entity_name} beweegt sech net", + "is_not_occupied": "{entity_name} ass fr\u00e4i", + "is_not_open": "{entity_name} ass zou", + "is_not_plugged_in": "{entity_name} ass net ugeschloss", + "is_not_powered": "{entity_name} ass net aliment\u00e9iert", + "is_not_present": "{entity_name} ass net pr\u00e4sent", + "is_not_unsafe": "{entity_name} ass s\u00e9cher", + "is_occupied": "{entity_name} ass besat", + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un", + "is_open": "{entity_name} ass op", + "is_plugged_in": "{entity_name} ass ugeschloss", + "is_powered": "{entity_name} ass aliment\u00e9iert", + "is_present": "{entity_name} ass pr\u00e4sent", + "is_problem": "{entity_name} entdeckt Problemer", + "is_smoke": "{entity_name} entdeckt Damp", + "is_sound": "{entity_name} entdeckt Toun", + "is_unsafe": "{entity_name} ass ons\u00e9cher", + "is_vibration": "{entity_name} entdeckt Vibratiounen" + }, + "trigger_type": { + "bat_low": "{entity_name} Batterie niddereg", + "closed": "{entity_name} gouf zougemaach", + "cold": "{entity_name} gouf kal", + "connected": "{entity_name} ass verbonnen", + "gas": "{entity_name} huet ugefaangen Gas z'entdecken", + "hot": "{entity_name} gouf waarm", + "light": "{entity_name} huet ugefange Luucht z'entdecken", + "locked": "{entity_name} gespaart", + "moist\u00a7": "{entity_name} gouf fiicht", + "motion": "{entity_name} huet ugefaange Beweegung z'entdecken", + "moving": "{entity_name} huet ugefaangen sech ze beweegen", + "no_gas": "{entity_name} huet opgehale Gas z'entdecken", + "no_light": "{entity_name} huet opgehale Luucht z'entdecken", + "no_motion": "{entity_name} huet opgehale Beweegung z'entdecken", + "no_problem": "{entity_name} huet opgehale Problemer z'entdecken", + "no_smoke": "{entity_name} huet opgehale Damp z'entdecken", + "no_sound": "{entity_name} huet opgehale Toun z'entdecken", + "no_vibration": "{entity_name} huet opgehale Vibratiounen z'entdecken", + "not_bat_low": "{entity_name} Batterie normal", + "not_cold": "{entity_name} gouf net kal", + "not_connected": "{entity_name} d\u00e9connect\u00e9iert", + "not_hot": "{entity_name} gouf net waarm", + "not_locked": "{entity_name} entspaart", + "not_moist": "{entity_name} gouf dr\u00e9chen", + "not_moving": "{entity_name} huet opgehale sech ze beweegen", + "not_occupied": "{entity_name} gouf fr\u00e4i", + "not_plugged_in": "{entity_name} net ugeschloss", + "not_powered": "{entity_name} net aliment\u00e9iert", + "not_present": "{entity_name} net pr\u00e4sent", + "not_unsafe": "{entity_name} gouf s\u00e9cher", + "occupied": "{entity_name} gouf besat", + "opened": "{entity_name} gouf opgemaach", + "plugged_in": "{entity_name} ugeschloss", + "powered": "{entity_name} aliment\u00e9iert", + "present": "{entity_name} pr\u00e4sent", + "problem": "{entity_name} huet ugefaange Problemer z'entdecken", + "smoke": "{entity_name} huet ugefaangen Damp z'entdecken", + "sound": "{entity_name} huet ugefaangen Toun z'entdecken", + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt", + "unsafe": "{entity_name} gouf ons\u00e9cher", + "vibration": "{entity_name} huet ugefaange Vibratiounen z'entdecken" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json new file mode 100644 index 00000000000000..139cff2187fcfb --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "Bateria {entity_name} jest roz\u0142adowana", + "is_cold": "{entity_name} wykrywa zimno", + "is_connected": "{entity_name} jest po\u0142\u0105czone", + "is_gas": "{entity_name} wykrywa gaz", + "is_hot": "{entity_name} wykrywa gor\u0105co", + "is_light": "{entity_name} wykrywa \u015bwiat\u0142o", + "is_locked": "{entity_name} jest zamkni\u0119te", + "is_moist": "{entity_name} wykrywa wilgo\u0107", + "is_motion": "{entity_name} wykrywa ruch", + "is_moving": "{entity_name} porusza si\u0119", + "is_no_gas": "{entity_name} nie wykrywa gazu", + "is_no_light": "{entity_name} nie wykrywa \u015bwiat\u0142a", + "is_no_motion": "{entity_name} nie wykrywa ruchu", + "is_off": "{entity_name} jest wy\u0142\u0105czone", + "is_on": "{entity_name} jest w\u0142\u0105czone" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/bg.json b/homeassistant/components/cert_expiry/.translations/bg.json new file mode 100644 index 00000000000000..7c82ef8b9ba8f0 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/bg.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "error": { + "certificate_fetch_failed": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043c\u0438\u0437\u0432\u043b\u0435\u0447\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442", + "connection_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u043e\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0442\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441", + "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "resolve_failed": "\u0422\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" + }, + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441 \u0432 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", + "port": "\u041f\u043e\u0440\u0442 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + }, + "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u0437\u0430 \u0442\u0435\u0441\u0442\u0432\u0430\u043d\u0435" + } + }, + "title": "\u0421\u0440\u043e\u043a \u043d\u0430 \u0432\u0430\u043b\u0438\u0434\u043d\u043e\u0441\u0442 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index f3eead4aae023a..c9963e496238fd 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -69,5 +69,23 @@ "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e" } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ CLIP \u0441\u0435\u043d\u0437\u043e\u0440\u0438", + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u043d\u0438 \u0433\u0440\u0443\u043f\u0438" + }, + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 \u0442\u0438\u043f\u043e\u0432\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ CLIP \u0441\u0435\u043d\u0437\u043e\u0440\u0438", + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 deCONZ \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u043d\u0438 \u0433\u0440\u0443\u043f\u0438" + }, + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 \u0442\u0438\u043f\u043e\u0432\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 1a03143f11edfd..840bc8929a7366 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -49,8 +49,8 @@ "button_3": "Dr\u00ebtte Kn\u00e4ppchen", "button_4": "V\u00e9ierte Kn\u00e4ppchen", "close": "Zoumaachen", - "dim_down": "Erhellen", - "dim_up": "Verd\u00e4ischteren", + "dim_down": "Verd\u00e4ischteren", + "dim_up": "Erhellen", "left": "L\u00e9nks", "open": "Op", "right": "Riets", diff --git a/homeassistant/components/ecobee/.translations/bg.json b/homeassistant/components/ecobee/.translations/bg.json new file mode 100644 index 00000000000000..bd8503fabd8b49 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "\u0422\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 ecobee." + }, + "error": { + "pin_request_failed": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0438\u0441\u043a\u0430\u043d\u0435 \u043d\u0430 \u041f\u0418\u041d \u043e\u0442 ecobee; \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u0430\u043b\u0438 API \u043a\u043b\u044e\u0447\u044a\u0442 \u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d.", + "token_request_failed": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0438\u0441\u043a\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434\u043e\u0432\u0435 \u043e\u0442 ecobee; \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + }, + "step": { + "authorize": { + "description": "\u041c\u043e\u043b\u044f, \u043e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0442\u043e\u0432\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 https://www.ecobee.com/consumerportal/index.html \u0441 \u043f\u0438\u043d \u043a\u043e\u0434: \n\n {pin} \n \n \u0421\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435.", + "title": "\u041e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 ecobee.com" + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 API \u043a\u043b\u044e\u0447\u0430, \u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043e\u0442 ecobee.com.", + "title": "ecobee API \u043a\u043b\u044e\u0447" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/en.json b/homeassistant/components/ecobee/.translations/en.json index 9e7e9fed39659a..39072f70d8202c 100644 --- a/homeassistant/components/ecobee/.translations/en.json +++ b/homeassistant/components/ecobee/.translations/en.json @@ -1,23 +1,25 @@ { "config": { - "title": "ecobee", - "step": { - "user": { - "title": "ecobee API key", - "description": "Please enter the API key obtained from ecobee.com.", - "data": {"api_key": "API Key"} - }, - "authorize": { - "title": "Authorize app on ecobee.com", - "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit." - } + "abort": { + "one_instance_only": "This integration currently supports only one ecobee instance." }, "error": { "pin_request_failed": "Error requesting PIN from ecobee; please verify API key is correct.", "token_request_failed": "Error requesting tokens from ecobee; please try again." }, - "abort": { - "one_instance_only": "This integration currently supports only one ecobee instance." - } + "step": { + "authorize": { + "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit.", + "title": "Authorize app on ecobee.com" + }, + "user": { + "data": { + "api_key": "API Key" + }, + "description": "Please enter the API key obtained from ecobee.com.", + "title": "ecobee API key" + } + }, + "title": "ecobee" } -} +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/bg.json b/homeassistant/components/geonetnz_quakes/.translations/bg.json new file mode 100644 index 00000000000000..48d6eacda917d9 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0437\u0430 \u0444\u0438\u043b\u0442\u044a\u0440\u0430 \u0441\u0438." + } + }, + "title": "GeoNet NZ \u0417\u0435\u043c\u0435\u0442\u0440\u0435\u0441\u0435\u043d\u0438\u044f" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/bg.json b/homeassistant/components/izone/.translations/bg.json new file mode 100644 index 00000000000000..26a55aa4ed8e79 --- /dev/null +++ b/homeassistant/components/izone/.translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 iZone \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430.", + "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 iZone." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/bg.json b/homeassistant/components/life360/.translations/bg.json index 4fae0249fd004a..02354204f2406f 100644 --- a/homeassistant/components/life360/.translations/bg.json +++ b/homeassistant/components/life360/.translations/bg.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438", "invalid_username": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "unexpected": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f \u0441\u044a\u0441 \u0441\u044a\u0440\u0432\u044a\u0440\u0430 Life360", "user_already_configured": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b" }, "step": { diff --git a/homeassistant/components/light/.translations/bg.json b/homeassistant/components/light/.translations/bg.json index 533ba76b6a7613..33b57d9e7cd1c1 100644 --- a/homeassistant/components/light/.translations/bg.json +++ b/homeassistant/components/light/.translations/bg.json @@ -8,6 +8,10 @@ "condition_type": { "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b.", "is_on": "{entity_name} \u0435 \u0432\u043a\u043b." + }, + "trigger_type": { + "turned_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "turned_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 22a93909578608..f8f4a2761d4624 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -6,7 +6,7 @@ "turn_on": "W\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "(entity_name} jest wy\u0142\u0105czony.", + "is_off": "{entity_name} jest wy\u0142\u0105czone", "is_on": "(entity_name} jest w\u0142\u0105czony." }, "trigger_type": { diff --git a/homeassistant/components/plex/.translations/bg.json b/homeassistant/components/plex/.translations/bg.json new file mode 100644 index 00000000000000..fb77e5da8cb666 --- /dev/null +++ b/homeassistant/components/plex/.translations/bg.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0432\u0435\u0447\u0435 \u0441\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438", + "already_configured": "\u0422\u043e\u0437\u0438 Plex \u0441\u044a\u0440\u0432\u044a\u0440 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "already_in_progress": "Plex \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", + "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430", + "unknown": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0440\u0430\u0434\u0438 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "faulty_credentials": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f", + "no_servers": "\u041d\u044f\u043c\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0438, \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441 \u0442\u043e\u0437\u0438 \u0430\u043a\u0430\u0443\u043d\u0442", + "no_token": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u043a\u043e\u0434 \u0438\u043b\u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "not_found": "Plex \u0441\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" + }, + "step": { + "manual_setup": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 SSL", + "token": "\u041a\u043e\u0434 (\u0430\u043a\u043e \u0441\u0435 \u0438\u0437\u0438\u0441\u043a\u0432\u0430)", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" + }, + "title": "Plex \u0441\u044a\u0440\u0432\u044a\u0440" + }, + "select_server": { + "data": { + "server": "\u0421\u044a\u0440\u0432\u044a\u0440" + }, + "description": "\u041d\u0430\u043b\u0438\u0447\u043d\u0438 \u0441\u0430 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0441\u044a\u0440\u0432\u044a\u0440\u0430, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u0438\u043d:", + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Plex \u0441\u044a\u0440\u0432\u044a\u0440" + }, + "user": { + "data": { + "manual_setup": "\u0420\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "token": "Plex \u043a\u043e\u0434" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043a\u043e\u0434 \u0437\u0430 Plex \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043b\u0438 \u0440\u044a\u0447\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440.", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 Plex \u0441\u044a\u0440\u0432\u044a\u0440" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index 58a5169ac02701..812de425ef49f2 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -13,6 +13,11 @@ "not_found": "Serveur Plex introuvable" }, "step": { + "manual_setup": { + "data": { + "port": "Port" + } + }, "select_server": { "data": { "server": "Serveur" diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 130cf2067abe72..244044b2f67a80 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "Feeler beider Autorisatioun", "no_servers": "Kee Server as mam Kont verbonnen", + "no_token": "Gitt en Token un oder wielt manuelle Setup", "not_found": "Kee Plex Server fonnt" }, "step": { + "manual_setup": { + "data": { + "host": "Apparat", + "port": "Port", + "ssl": "SSL benotzen", + "token": "Jeton (falls n\u00e9ideg)", + "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen" + }, + "title": "Plex Server" + }, "select_server": { "data": { "server": "Server" @@ -22,6 +33,7 @@ }, "user": { "data": { + "manual_setup": "Manuell Konfiguratioun", "token": "Jeton fir de Plex" }, "description": "Gitt een Jeton fir de Plex un fir eng automatesch Konfiguratioun", diff --git a/homeassistant/components/solaredge/.translations/bg.json b/homeassistant/components/solaredge/.translations/bg.json index 72f1ad2a4c758f..e4223e373fd537 100644 --- a/homeassistant/components/solaredge/.translations/bg.json +++ b/homeassistant/components/solaredge/.translations/bg.json @@ -15,6 +15,7 @@ }, "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 (API) \u0437\u0430 \u0442\u0430\u0437\u0438 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f" } - } + }, + "title": "SolarEdge" } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/bg.json b/homeassistant/components/switch/.translations/bg.json index efccc652d5beb7..64a3ea94e1b543 100644 --- a/homeassistant/components/switch/.translations/bg.json +++ b/homeassistant/components/switch/.translations/bg.json @@ -6,6 +6,8 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" }, "condition_type": { + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" }, diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index 199b150f68ee6d..31187aaa1b7cc4 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -6,8 +6,8 @@ "turn_on": "W\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "{entity_name} jest wy\u0142\u0105czony.", - "is_on": "{entity_name} jest w\u0142\u0105czony", + "is_off": "{entity_name} jest wy\u0142\u0105czone", + "is_on": "{entity_name} jest w\u0142\u0105czone", "turn_off": "{entity_name} wy\u0142\u0105czone", "turn_on": "{entity_name} w\u0142\u0105czone" }, diff --git a/homeassistant/components/toon/.translations/bg.json b/homeassistant/components/toon/.translations/bg.json index e4aa0d8c08842a..0de9452b3cd226 100644 --- a/homeassistant/components/toon/.translations/bg.json +++ b/homeassistant/components/toon/.translations/bg.json @@ -15,6 +15,7 @@ "authenticate": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "tenant": "\u041d\u0430\u0435\u043c\u0430\u0442\u0435\u043b", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, "description": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0441 \u0412\u0430\u0448\u0438\u044f Eneco Toon \u043f\u0440\u043e\u0444\u0438\u043b (\u043d\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0437\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u0446\u0438).", diff --git a/homeassistant/components/traccar/.translations/bg.json b/homeassistant/components/traccar/.translations/bg.json new file mode 100644 index 00000000000000..7fe89d491c9552 --- /dev/null +++ b/homeassistant/components/traccar/.translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u0435\u043d \u043e\u0442 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0437\u0430 \u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430 \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e\u0442 Traccar.", + "one_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "create_entry": { + "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 Traccar. \n\n \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0438\u044f \u0430\u0434\u0440\u0435\u0441: ` {webhook_url} ` \n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." + }, + "step": { + "user": { + "description": "\u0421\u0438\u0433\u0443\u0440\u043d\u0438 \u043b\u0438 \u0441\u0442\u0435, \u0447\u0435 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Traccar?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/bg.json b/homeassistant/components/twentemilieu/.translations/bg.json new file mode 100644 index 00000000000000..df36ab070d7991 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d." + }, + "error": { + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435.", + "invalid_address": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d \u0432 \u0437\u043e\u043d\u0430 \u0437\u0430 \u043e\u0431\u0441\u043b\u0443\u0436\u0432\u0430\u043d\u0435 \u043d\u0430 Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "\u0414\u043e\u043c\u0430\u0448\u043d\u043e \u043f\u0438\u0441\u043c\u043e/\u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u043e", + "house_number": "\u041d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043a\u044a\u0449\u0430", + "post_code": "\u041f\u043e\u0449\u0435\u043d\u0441\u043a\u0438 \u043a\u043e\u0434" + }, + "description": "\u0421\u044a\u0437\u0434\u0430\u0439\u0442\u0435 Twente Milieu, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u044f\u0449\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u0441\u044a\u0431\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u0442\u043f\u0430\u0434\u044a\u0446\u0438 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0430\u0434\u0440\u0435\u0441.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/bg.json b/homeassistant/components/unifi/.translations/bg.json index d8f571c968e01c..df5654ff78be5b 100644 --- a/homeassistant/components/unifi/.translations/bg.json +++ b/homeassistant/components/unifi/.translations/bg.json @@ -22,5 +22,17 @@ } }, "title": "UniFi \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "\u0412\u0440\u0435\u043c\u0435 \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0438 \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u043e\u0442\u043e \u0432\u0438\u0436\u0434\u0430\u043d\u0435 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0447\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043a\u0430\u0442\u043e \u043e\u0442\u0441\u044a\u0441\u0442\u0432\u0430\u0449\u043e", + "track_clients": "\u041f\u0440\u043e\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u0438", + "track_devices": "\u041f\u0440\u043e\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (Ubiquiti \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430)", + "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0435\u0442\u0435 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441 \u043a\u0430\u0431\u0435\u043b" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/bg.json b/homeassistant/components/velbus/.translations/bg.json new file mode 100644 index 00000000000000..e769f83d28e2f6 --- /dev/null +++ b/homeassistant/components/velbus/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "\u0422\u043e\u0437\u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "connection_failed": "\u0412\u0440\u044a\u0437\u043a\u0430\u0442\u0430 \u0441 velbus \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "port_exists": "\u0422\u043e\u0437\u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0430 \u0442\u0430\u0437\u0438 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 velbus", + "port": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u0449 \u043d\u0438\u0437" + }, + "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0442\u0438\u043f\u0430 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430 \u0441 velbus" + } + }, + "title": "Velbus \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/bg.json b/homeassistant/components/vesync/.translations/bg.json new file mode 100644 index 00000000000000..a12436936e6960 --- /dev/null +++ b/homeassistant/components/vesync/.translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Vesync" + }, + "error": { + "invalid_login": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "E-mail \u0430\u0434\u0440\u0435\u0441" + }, + "title": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/bg.json b/homeassistant/components/withings/.translations/bg.json new file mode 100644 index 00000000000000..e75860d0e16b6f --- /dev/null +++ b/homeassistant/components/withings/.translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "no_flows": "\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Withings, \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0441\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0442\u0435. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Withings \u0437\u0430 \u0438\u0437\u0431\u0440\u0430\u043d\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b." + }, + "step": { + "user": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b, \u043a\u044a\u043c \u043a\u043e\u0439\u0442\u043e \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 Home Assistant \u0441 Withings. \u041d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u043d\u0430 Withings \u043d\u0435 \u0437\u0430\u0431\u0440\u0430\u0432\u044f\u0439\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u0438\u043d \u0438 \u0441\u044a\u0449 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b \u0438\u043b\u0438 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u043d\u044f\u043c\u0430 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e.", + "title": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/bg.json b/homeassistant/components/zha/.translations/bg.json index 642a2c0af134a6..2715ef46dc8504 100644 --- a/homeassistant/components/zha/.translations/bg.json +++ b/homeassistant/components/zha/.translations/bg.json @@ -16,5 +16,48 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430", + "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_5": "\u041f\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_6": "\u0428\u0435\u0441\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "close": "\u0417\u0430\u0442\u0432\u043e\u0440\u0438", + "dim_down": "\u0417\u0430\u0442\u044a\u043c\u043d\u044f\u0432\u0430\u043d\u0435", + "dim_up": "\u041e\u0441\u0432\u0435\u0442\u044f\u0432\u0430\u043d\u0435", + "face_1": "\u0441 \u043b\u0438\u0446\u0435 1 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_2": "\u0441 \u043b\u0438\u0446\u0435 2 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_3": "\u0441 \u043b\u0438\u0446\u0435 3 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_4": "\u0441 \u043b\u0438\u0446\u0435 4 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_5": "\u0441 \u043b\u0438\u0446\u0435 5 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_6": "\u0441 \u043b\u0438\u0446\u0435 6 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_any": "\u0421 \u043d\u044f\u043a\u043e\u0438/\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438 \u043b\u0438\u0446\u0435(\u0430) \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0438", + "left": "\u041b\u044f\u0432\u043e", + "open": "\u041e\u0442\u0432\u043e\u0440\u0435\u043d", + "right": "\u0414\u044f\u0441\u043d\u043e", + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438" + }, + "trigger_type": { + "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0438\u0437\u0442\u044a\u0440\u0432\u0430\u043d\u043e", + "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043e\u0431\u044a\u0440\u043d\u0430\u0442\u043e \"{subtype}\"", + "device_knocked": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043f\u043e\u0447\u0443\u043a\u0430\u043d\u043e \"{subtype}\"", + "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \"{subtype}\"", + "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e", + "device_slid": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0433\u043e \u0435 \u043f\u043b\u044a\u0437\u043d\u0430\u0442\u043e \"{subtype}\"", + "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043d\u0430\u043a\u043b\u043e\u043d\u0435\u043d\u043e", + "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e", + "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", + "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", + "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json index 3140648f57adf6..8d99b6eebc9db8 100644 --- a/homeassistant/components/zha/.translations/da.json +++ b/homeassistant/components/zha/.translations/da.json @@ -16,5 +16,22 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Begge knapper", + "button_1": "F\u00f8rste knap", + "button_2": "Anden knap", + "button_3": "Tredje knap", + "button_4": "Fjerde knap", + "button_5": "Femte knap", + "close": "Luk", + "left": "Venstre", + "open": "\u00c5ben", + "right": "H\u00f8jre" + }, + "trigger_type": { + "device_shaken": "Enhed rystet" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/lb.json b/homeassistant/components/zha/.translations/lb.json index f3e7053ca11bc6..49a754f1da587f 100644 --- a/homeassistant/components/zha/.translations/lb.json +++ b/homeassistant/components/zha/.translations/lb.json @@ -16,5 +16,48 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "B\u00e9id Kn\u00e4ppchen", + "button_1": "\u00c9ischte Kn\u00e4ppchen", + "button_2": "Zweete Kn\u00e4ppchen", + "button_3": "Dr\u00ebtte Kn\u00e4ppchen", + "button_4": "V\u00e9ierte Kn\u00e4ppchen", + "button_5": "F\u00ebnnefte Kn\u00e4ppchen", + "button_6": "Sechste Kn\u00e4ppchen", + "close": "Zoumaachen", + "dim_down": "Verd\u00e4ischteren", + "dim_up": "Erhellen", + "face_1": "mat S\u00e4it 1 aktiv\u00e9iert", + "face_2": "mat S\u00e4it 2 aktiv\u00e9iert", + "face_3": "mat S\u00e4it 3 aktiv\u00e9iert", + "face_4": "mat S\u00e4it 4 aktiv\u00e9iert", + "face_5": "mat S\u00e4it 5 aktiv\u00e9iert", + "face_6": "mat S\u00e4it 6 aktiv\u00e9iert", + "face_any": "Mat iergendenger/spezifiz\u00e9ierter S\u00e4it(en) aktiv\u00e9iert", + "left": "L\u00e9nks", + "open": "Op", + "right": "Riets", + "turn_off": "Ausschalten", + "turn_on": "Uschalten" + }, + "trigger_type": { + "device_dropped": "Apparat gefall", + "device_flipped": "Apparat \u00ebmgedr\u00e9int \"{subtype}\"", + "device_knocked": "Apparat geklappt \"{subtype}\"", + "device_rotated": "Apparat gedr\u00e9int \"{subtype}\"", + "device_shaken": "Apparat ger\u00ebselt", + "device_slid": "Apparat gerutscht \"{subtype}\"", + "device_tilted": "Apparat ass gekippt", + "remote_button_double_press": "\"{subtype}\" Kn\u00e4ppche zwee mol gedr\u00e9ckt", + "remote_button_long_press": "\"{subtype}\" Kn\u00e4ppche permanent gedr\u00e9ckt", + "remote_button_long_release": "\"{subtype}\" Kn\u00e4ppche no laangem unhalen lassgelooss", + "remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt", + "remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt", + "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", + "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", + "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nl.json b/homeassistant/components/zha/.translations/nl.json index 56c2518f11e5b8..5e5c666b1a4e8d 100644 --- a/homeassistant/components/zha/.translations/nl.json +++ b/homeassistant/components/zha/.translations/nl.json @@ -16,5 +16,31 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "left": "Links", + "open": "Open", + "right": "Rechts", + "turn_off": "Uitschakelen", + "turn_on": "Inschakelen" + }, + "trigger_type": { + "device_dropped": "Apparaat gevallen", + "device_flipped": "Apparaat omgedraaid \"{subtype}\"", + "device_knocked": "Apparaat klopte \"{subtype}\"", + "device_rotated": "Apparaat gedraaid \" {subtype} \"", + "device_shaken": "Apparaat geschud", + "device_slid": "Apparaat geschoven \"{subtype}\"\".", + "device_tilted": "Apparaat gekanteld", + "remote_button_double_press": "\"{subtype}\" knop dubbel geklikt", + "remote_button_long_press": "\" {subtype} \" knop continu ingedrukt", + "remote_button_long_release": "\"{subtype}\" knop losgelaten na lang indrukken van de knop", + "remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt", + "remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt", + "remote_button_short_press": "\" {subtype} \" knop ingedrukt", + "remote_button_short_release": "\"{subtype}\" knop losgelaten", + "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index f639c85c682612..36bdfb6d5d33a5 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -24,9 +24,18 @@ "button_2": "Andre knapp", "button_3": "Tredje knapp", "button_4": "Fjerde knapp", + "button_5": "Femte knapp", + "button_6": "Sjette knapp", "close": "Lukk", "dim_down": "Dimm ned", "dim_up": "Dimm opp", + "face_1": "med ansikt 1 aktivert", + "face_2": "med ansikt 2 aktivert", + "face_3": "med ansikt 3 aktivert", + "face_4": "med ansikt 4 aktivert", + "face_5": "med ansikt 5 aktivert", + "face_6": "med ansikt 6 aktivert", + "face_any": "Med alle/angitte ansikt (er) aktivert", "left": "Venstre", "open": "\u00c5pen", "right": "H\u00f8yre", @@ -34,8 +43,21 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { + "device_dropped": "Enheten ble brutt", + "device_flipped": "Enheten snudd \"{undertype}\"", + "device_knocked": "Enheten sl\u00e5tt \"{undertype}\"", + "device_rotated": "Enheten roterte \"{under type}\"", + "device_shaken": "Enhet er ristet", + "device_slid": "Enheten skled \"{undertype}\"", + "device_tilted": "Enhet vippet", + "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", + "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", + "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", + "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\" {subtype} \" - knappen ble femdobbelt klikket", "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", - "remote_button_short_release": "\"{subtype}\"-knappen sluppet" + "remote_button_short_release": "\"{subtype}\"-knappen sluppet", + "remote_button_triple_press": "\" {subtype} \"-knappen ble rippel klikket" } } } \ No newline at end of file From 770ad86f126eb890f6c4021dc72261af62acd862 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 26 Sep 2019 07:42:46 +0200 Subject: [PATCH 168/296] Add mysensors codeowner (#26917) --- CODEOWNERS | 1 + homeassistant/components/mysensors/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index cb9180d717daa8..6abb7535574bac 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -182,6 +182,7 @@ homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core +homeassistant/components/mysensors/* @MartinHjelmare homeassistant/components/mystrom/* @fabaff homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index f18f5d4f8dda60..536848d3aef645 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -9,5 +9,7 @@ "after_dependencies": [ "mqtt" ], - "codeowners": [] + "codeowners": [ + "@MartinHjelmare" + ] } From 62cea2b7ac061d7cf819b0a004b2750e8e98ac70 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Thu, 26 Sep 2019 01:53:31 -0700 Subject: [PATCH 169/296] Bump pyobihai, add unique ID and availability (#26922) * Bump pyobihai, add unique ID and availability * Fix unique ID * Fetch serial correctly --- homeassistant/components/obihai/manifest.json | 2 +- homeassistant/components/obihai/sensor.py | 46 +++++++++++-------- requirements_all.txt | 2 +- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index dd4df479af431c..68045ff0584684 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -3,7 +3,7 @@ "name": "Obihai", "documentation": "https://www.home-assistant.io/components/obihai", "requirements": [ - "pyobihai==1.1.1" + "pyobihai==1.2.0" ], "dependencies": [], "codeowners": ["@dshokouhi"] diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index fbf4fffb17f807..4644875ee8b164 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -44,27 +44,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] - pyobihai = PyObihai() + pyobihai = PyObihai(host, username, password) - login = pyobihai.check_account(host, username, password) + login = pyobihai.check_account() if not login: _LOGGER.error("Invalid credentials") return - services = pyobihai.get_state(host, username, password) + serial = pyobihai.get_device_serial() - line_services = pyobihai.get_line_state(host, username, password) + services = pyobihai.get_state() - call_direction = pyobihai.get_call_direction(host, username, password) + line_services = pyobihai.get_line_state() + + call_direction = pyobihai.get_call_direction() for key in services: - sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) for key in line_services: - sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) for key in call_direction: - sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) add_entities(sensors) @@ -72,15 +74,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ObihaiServiceSensors(Entity): """Get the status of each Obihai Lines.""" - def __init__(self, pyobihai, host, username, password, service_name): + def __init__(self, pyobihai, serial, service_name): """Initialize monitor sensor.""" - self._host = host - self._username = username - self._password = password self._service_name = service_name self._state = None self._name = f"{OBIHAI} {self._service_name}" self._pyobihai = pyobihai + self._unique_id = f"{serial}-{self._service_name}" @property def name(self): @@ -92,6 +92,18 @@ def state(self): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Return if sensor is available.""" + if self._state is not None: + return True + return False + + @property + def unique_id(self): + """Return the unique ID.""" + return self._unique_id + @property def device_class(self): """Return the device class for uptime sensor.""" @@ -101,21 +113,17 @@ def device_class(self): def update(self): """Update the sensor.""" - services = self._pyobihai.get_state(self._host, self._username, self._password) + services = self._pyobihai.get_state() if self._service_name in services: self._state = services.get(self._service_name) - services = self._pyobihai.get_line_state( - self._host, self._username, self._password - ) + services = self._pyobihai.get_line_state() if self._service_name in services: self._state = services.get(self._service_name) - call_direction = self._pyobihai.get_call_direction( - self._host, self._username, self._password - ) + call_direction = self._pyobihai.get_call_direction() if self._service_name in call_direction: self._state = call_direction.get(self._service_name) diff --git a/requirements_all.txt b/requirements_all.txt index 6481137f3e6a51..5bbd77f4d01b91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1346,7 +1346,7 @@ pynx584==0.4 pynzbgetapi==0.2.0 # homeassistant.components.obihai -pyobihai==1.1.1 +pyobihai==1.2.0 # homeassistant.components.ombi pyombi==0.1.5 From 9b204ad1629ffb0e2e14f28a547eeaa30dd4a1b1 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 26 Sep 2019 04:10:20 -0500 Subject: [PATCH 170/296] Add Plex config options support (#26870) * Add config options support * Actually copy dict * Move media_player options to PlexServer class * Handle updated config options * Move callback out of server --- homeassistant/components/plex/__init__.py | 23 ++++++-- homeassistant/components/plex/config_flow.py | 49 +++++++++++++++++ homeassistant/components/plex/media_player.py | 20 +++---- homeassistant/components/plex/server.py | 21 ++++++- homeassistant/components/plex/strings.json | 11 ++++ tests/components/plex/test_config_flow.py | 55 +++++++++++++++++++ 6 files changed, 160 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index dd458dda07880a..874ac6334acf5b 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -77,7 +77,7 @@ def _setup_plex(hass, config): """Pass configuration to a config flow.""" server_config = dict(config) if MP_DOMAIN in server_config: - hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = server_config.pop(MP_DOMAIN) + hass.data.setdefault(PLEX_MEDIA_PLAYER_OPTIONS, server_config.pop(MP_DOMAIN)) if CONF_HOST in server_config: prefix = "https" if server_config.pop(CONF_SSL) else "http" server_config[ @@ -96,7 +96,15 @@ async def async_setup_entry(hass, entry): """Set up Plex from a config entry.""" server_config = entry.data[PLEX_SERVER_CONFIG] - plex_server = PlexServer(server_config) + if MP_DOMAIN not in entry.options: + options = dict(entry.options) + options.setdefault( + MP_DOMAIN, + hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS) or MEDIA_PLAYER_SCHEMA({}), + ) + hass.config_entries.async_update_entry(entry, options=options) + + plex_server = PlexServer(server_config, entry.options) try: await hass.async_add_executor_job(plex_server.connect) except requests.exceptions.ConnectionError as error: @@ -123,14 +131,13 @@ async def async_setup_entry(hass, entry): ) hass.data[PLEX_DOMAIN][SERVERS][plex_server.machine_identifier] = plex_server - if not hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS): - hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = MEDIA_PLAYER_SCHEMA({}) - for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) ) + entry.add_update_listener(async_options_updated) + return True @@ -150,3 +157,9 @@ async def async_unload_entry(hass, entry): hass.data[PLEX_DOMAIN][SERVERS].pop(server_id) return True + + +async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + server_id = entry.data[CONF_SERVER_IDENTIFIER] + hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index e620e4869e5083..cf70b7470cdbcc 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Plex.""" +import copy import logging import plexapi.exceptions @@ -6,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( CONF_HOST, CONF_PORT, @@ -20,6 +22,8 @@ from .const import ( # pylint: disable=unused-import CONF_SERVER, CONF_SERVER_IDENTIFIER, + CONF_USE_EPISODE_ART, + CONF_SHOW_ALL_CONTROLS, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, @@ -52,6 +56,12 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return PlexOptionsFlowHandler(config_entry) + def __init__(self): """Initialize the Plex flow.""" self.current_login = {} @@ -214,3 +224,42 @@ async def async_step_import(self, import_config): """Import from Plex configuration.""" _LOGGER.debug("Imported Plex configuration") return await self.async_step_server_validate(import_config) + + +class PlexOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Plex options.""" + + def __init__(self, config_entry): + """Initialize Plex options flow.""" + self.options = copy.deepcopy(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the Plex options.""" + return await self.async_step_plex_mp_settings() + + async def async_step_plex_mp_settings(self, user_input=None): + """Manage the Plex media_player options.""" + if user_input is not None: + self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] = user_input[ + CONF_USE_EPISODE_ART + ] + self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] = user_input[ + CONF_SHOW_ALL_CONTROLS + ] + return self.async_create_entry(title="", data=self.options) + + return self.async_show_form( + step_id="plex_mp_settings", + data_schema=vol.Schema( + { + vol.Required( + CONF_USE_EPISODE_ART, + default=self.options[MP_DOMAIN][CONF_USE_EPISODE_ART], + ): bool, + vol.Required( + CONF_SHOW_ALL_CONTROLS, + default=self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS], + ): bool, + } + ), + ) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4d097253ea1a93..356c7fe5741702 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -33,12 +33,9 @@ from homeassistant.util import dt as dt_util from .const import ( - CONF_USE_EPISODE_ART, - CONF_SHOW_ALL_CONTROLS, CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, - PLEX_MEDIA_PLAYER_OPTIONS, REFRESH_LISTENERS, SERVERS, ) @@ -67,8 +64,6 @@ def add_entities(entities, update_before_add=False): def _setup_platform(hass, config_entry, add_entities_callback): """Set up the Plex media_player platform.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] - config = hass.data[PLEX_MEDIA_PLAYER_OPTIONS] - plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] plex_clients = {} plex_sessions = {} @@ -102,7 +97,7 @@ def update_devices(): if device.machineIdentifier not in plex_clients: new_client = PlexClient( - config, device, None, plex_sessions, update_devices + plexserver, device, None, plex_sessions, update_devices ) plex_clients[device.machineIdentifier] = new_client _LOGGER.debug("New device: %s", device.machineIdentifier) @@ -141,7 +136,7 @@ def update_devices(): and machine_identifier is not None ): new_client = PlexClient( - config, player, session, plex_sessions, update_devices + plexserver, player, session, plex_sessions, update_devices ) plex_clients[machine_identifier] = new_client _LOGGER.debug("New session: %s", machine_identifier) @@ -170,7 +165,7 @@ def update_devices(): class PlexClient(MediaPlayerDevice): """Representation of a Plex device.""" - def __init__(self, config, device, session, plex_sessions, update_devices): + def __init__(self, plex_server, device, session, plex_sessions, update_devices): """Initialize the Plex device.""" self._app_name = "" self._device = None @@ -191,7 +186,7 @@ def __init__(self, config, device, session, plex_sessions, update_devices): self._state = STATE_IDLE self._volume_level = 1 # since we can't retrieve remotely self._volume_muted = False # since we can't retrieve remotely - self.config = config + self.plex_server = plex_server self.plex_sessions = plex_sessions self.update_devices = update_devices # General @@ -317,8 +312,9 @@ def refresh(self, device, session): def _set_media_image(self): thumb_url = self._session.thumbUrl - if self.media_content_type is MEDIA_TYPE_TVSHOW and not self.config.get( - CONF_USE_EPISODE_ART + if ( + self.media_content_type is MEDIA_TYPE_TVSHOW + and not self.plex_server.use_episode_art ): thumb_url = self._session.url(self._session.grandparentThumb) @@ -551,7 +547,7 @@ def supported_features(self): return 0 # force show all controls - if self.config.get(CONF_SHOW_ALL_CONTROLS): + if self.plex_server.show_all_controls: return ( SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index f41a9bdabae183..09274472915235 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -3,22 +3,29 @@ import plexapi.server from requests import Session +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL -from .const import CONF_SERVER, DEFAULT_VERIFY_SSL +from .const import ( + CONF_SERVER, + CONF_SHOW_ALL_CONTROLS, + CONF_USE_EPISODE_ART, + DEFAULT_VERIFY_SSL, +) from .errors import NoServersFound, ServerNotSpecified class PlexServer: """Manages a single Plex server connection.""" - def __init__(self, server_config): + def __init__(self, server_config, options=None): """Initialize a Plex server instance.""" self._plex_server = None self._url = server_config.get(CONF_URL) self._token = server_config.get(CONF_TOKEN) self._server_name = server_config.get(CONF_SERVER) self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) + self.options = options def connect(self): """Connect to a Plex server directly, obtaining direct URL if necessary.""" @@ -80,3 +87,13 @@ def machine_identifier(self): def url_in_use(self): """Return URL used for connected Plex server.""" return self._plex_server._baseurl # pylint: disable=W0212 + + @property + def use_episode_art(self): + """Return use_episode_art option.""" + return self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] + + @property + def show_all_controls(self): + """Return show_all_controls option.""" + return self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index c093d4fe0cec1e..812e7b81a7cf24 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -41,5 +41,16 @@ "invalid_import": "Imported configuration is invalid", "unknown": "Failed for unknown reason" } + }, + "options": { + "step": { + "plex_mp_settings": { + "description": "Options for Plex Media Players", + "data": { + "use_episode_art": "Use episode art", + "show_all_controls": "Show all controls" + } + } + } } } diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index e98aed793cfdca..37cf0fa200cf94 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -28,6 +28,13 @@ MOCK_SERVER_1 = MockAvailableServer(MOCK_NAME_1, MOCK_ID_1) MOCK_SERVER_2 = MockAvailableServer(MOCK_NAME_2, MOCK_ID_2) +DEFAULT_OPTIONS = { + config_flow.MP_DOMAIN: { + config_flow.CONF_USE_EPISODE_ART: False, + config_flow.CONF_SHOW_ALL_CONTROLS: False, + } +} + def init_config_flow(hass): """Init a configuration flow.""" @@ -520,3 +527,51 @@ async def test_manual_config(hass): == mock_connections.connections[0].httpuri ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + + +async def test_no_token(hass): + """Test failing when no token provided.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": False} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"][CONF_TOKEN] == "no_token" + + +async def test_option_flow(hass): + """Test config flow selection of one of two bridges.""" + + entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=DEFAULT_OPTIONS) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.flow.async_init( + entry.entry_id, context={"source": "test"}, data=None + ) + + assert result["type"] == "form" + assert result["step_id"] == "plex_mp_settings" + + result = await hass.config_entries.options.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_USE_EPISODE_ART: True, + config_flow.CONF_SHOW_ALL_CONTROLS: True, + }, + ) + assert result["type"] == "create_entry" + assert result["data"] == { + config_flow.MP_DOMAIN: { + config_flow.CONF_USE_EPISODE_ART: True, + config_flow.CONF_SHOW_ALL_CONTROLS: True, + } + } From 82b77c2d29c95609d9ebdec0cbaf329cbb1d306d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 26 Sep 2019 12:14:57 +0300 Subject: [PATCH 171/296] Add config flow to transmission (#26434) * Add config flow to transmission * Reworked code to add all sensors and switches * applied fixes * final touches * Add tests * fixed tests * fix get_api errors and entities availabilty update * update config_flows.py * fix pylint error * update .coveragerc * add codeowner * add test_options * fixed test_options --- .coveragerc | 6 +- CODEOWNERS | 1 + .../transmission/.translations/en.json | 40 +++ .../components/transmission/__init__.py | 244 +++++++++++------ .../components/transmission/config_flow.py | 104 ++++++++ .../components/transmission/const.py | 24 ++ .../components/transmission/errors.py | 14 + .../components/transmission/manifest.json | 7 +- .../components/transmission/sensor.py | 21 +- .../components/transmission/services.yaml | 2 +- .../components/transmission/strings.json | 40 +++ .../components/transmission/switch.py | 67 +++-- homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/transmission/__init__.py | 1 + .../transmission/test_config_flow.py | 245 ++++++++++++++++++ 17 files changed, 708 insertions(+), 113 deletions(-) create mode 100644 homeassistant/components/transmission/.translations/en.json create mode 100644 homeassistant/components/transmission/config_flow.py create mode 100644 homeassistant/components/transmission/const.py create mode 100644 homeassistant/components/transmission/errors.py create mode 100644 homeassistant/components/transmission/strings.json create mode 100644 tests/components/transmission/__init__.py create mode 100644 tests/components/transmission/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index a8932f54a54725..d42d7cbb3b3250 100644 --- a/.coveragerc +++ b/.coveragerc @@ -675,7 +675,11 @@ omit = homeassistant/components/tradfri/cover.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py - homeassistant/components/transmission/* + homeassistant/components/transmission/__init__.py + homeassistant/components/transmission/sensor.py + homeassistant/components/transmission/switch.py + homeassistant/components/transmission/const.py + homeassistant/components/transmission/errors.py homeassistant/components/travisci/sensor.py homeassistant/components/tuya/* homeassistant/components/twentemilieu/const.py diff --git a/CODEOWNERS b/CODEOWNERS index 6abb7535574bac..11b99b42a44ac1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -288,6 +288,7 @@ homeassistant/components/tplink/* @rytilahti homeassistant/components/traccar/* @ludeeus homeassistant/components/tradfri/* @ggravlingen homeassistant/components/trafikverket_train/* @endor-force +homeassistant/components/transmission/* @engrbm87 homeassistant/components/tts/* @robbiet480 homeassistant/components/twentemilieu/* @frenck homeassistant/components/twilio_call/* @robbiet480 diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json new file mode 100644 index 00000000000000..7160cd109c4794 --- /dev/null +++ b/homeassistant/components/transmission/.translations/en.json @@ -0,0 +1,40 @@ +{ + "config": { + "title": "Transmission", + "step": { + "user": { + "title": "Setup Transmission Client", + "data": { + "name": "Name", + "host": "Host", + "username": "User name", + "password": "Password", + "port": "Port" + } + }, + "options": { + "title": "Configure Options", + "data": { + "scan_interval": "Update frequency" + } + } + }, + "error": { + "wrong_credentials": "Wrong username or password", + "cannot_connect": "Unable to Connect to host" + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for Transmission", + "data": { + "scan_interval": "Update frequency" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index e7f9b94046d367..e6ddd87bdf56bd 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -2,47 +2,38 @@ from datetime import timedelta import logging +import transmissionrpc +from transmissionrpc.error import TransmissionError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_HOST, - CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + ATTR_TORRENT, + DATA_TRANSMISSION, + DATA_UPDATED, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + SERVICE_ADD_TORRENT, +) +from .errors import AuthenticationError, CannotConnect, UnknownError _LOGGER = logging.getLogger(__name__) -DOMAIN = "transmission" -DATA_UPDATED = "transmission_data_updated" -DATA_TRANSMISSION = "data_transmission" - -DEFAULT_NAME = "Transmission" -DEFAULT_PORT = 9091 -TURTLE_MODE = "turtle_mode" - -SENSOR_TYPES = { - "active_torrents": ["Active Torrents", None], - "current_status": ["Status", None], - "download_speed": ["Down Speed", "MB/s"], - "paused_torrents": ["Paused Torrents", None], - "total_torrents": ["Total Torrents", None], - "upload_speed": ["Up Speed", "MB/s"], - "completed_torrents": ["Completed Torrents", None], - "started_torrents": ["Started Torrents", None], -} - -DEFAULT_SCAN_INTERVAL = timedelta(seconds=120) - -ATTR_TORRENT = "torrent" - -SERVICE_ADD_TORRENT = "add_torrent" SERVICE_ADD_TORRENT_SCHEMA = vol.Schema({vol.Required(ATTR_TORRENT): cv.string}) @@ -55,13 +46,9 @@ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(TURTLE_MODE, default=False): cv.boolean, vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL ): cv.time_period, - vol.Optional( - CONF_MONITORED_CONDITIONS, default=["current_status"] - ): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), } ) }, @@ -69,63 +56,157 @@ ) -def setup(hass, config): +async def async_setup(hass, config): + """Import the Transmission Component from config.""" + if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): """Set up the Transmission Component.""" - host = config[DOMAIN][CONF_HOST] - username = config[DOMAIN].get(CONF_USERNAME) - password = config[DOMAIN].get(CONF_PASSWORD) - port = config[DOMAIN][CONF_PORT] - scan_interval = config[DOMAIN][CONF_SCAN_INTERVAL] + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + + if not config_entry.options: + await async_populate_options(hass, config_entry) + + client = TransmissionClient(hass, config_entry) + client_id = config_entry.entry_id + hass.data[DOMAIN][client_id] = client + if not await client.async_setup(): + return False + + return True + + +async def async_unload_entry(hass, entry): + """Unload Transmission Entry from config_entry.""" + hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) + if hass.data[DOMAIN][entry.entry_id].unsub_timer: + hass.data[DOMAIN][entry.entry_id].unsub_timer() + + for component in "sensor", "switch": + await hass.config_entries.async_forward_entry_unload(entry, component) + + del hass.data[DOMAIN] + + return True - import transmissionrpc - from transmissionrpc.error import TransmissionError +async def get_api(hass, host, port, username=None, password=None): + """Get Transmission client.""" try: - api = transmissionrpc.Client(host, port=port, user=username, password=password) - api.session_stats() + api = await hass.async_add_executor_job( + transmissionrpc.Client, host, port, username, password + ) + return api + except TransmissionError as error: - if str(error).find("401: Unauthorized"): - _LOGGER.error("Credentials for" " Transmission client are not valid") - return False + if "401: Unauthorized" in str(error): + _LOGGER.error("Credentials for Transmission client are not valid") + raise AuthenticationError + if "111: Connection refused" in str(error): + _LOGGER.error("Connecting to the Transmission client failed") + raise CannotConnect + + _LOGGER.error(error) + raise UnknownError + + +async def async_populate_options(hass, config_entry): + """Populate default options for Transmission Client.""" + options = {CONF_SCAN_INTERVAL: config_entry.data["options"][CONF_SCAN_INTERVAL]} - tm_data = hass.data[DATA_TRANSMISSION] = TransmissionData(hass, config, api) + hass.config_entries.async_update_entry(config_entry, options=options) - tm_data.update() - tm_data.init_torrent_list() - def refresh(event_time): - """Get the latest data from Transmission.""" - tm_data.update() +class TransmissionClient: + """Transmission Client Object.""" - track_time_interval(hass, refresh, scan_interval) + def __init__(self, hass, config_entry): + """Initialize the Transmission RPC API.""" + self.hass = hass + self.config_entry = config_entry + self.scan_interval = self.config_entry.options[CONF_SCAN_INTERVAL] + self.tm_data = None + self.unsub_timer = None + + async def async_setup(self): + """Set up the Transmission client.""" + + config = { + CONF_HOST: self.config_entry.data[CONF_HOST], + CONF_PORT: self.config_entry.data[CONF_PORT], + CONF_USERNAME: self.config_entry.data.get(CONF_USERNAME), + CONF_PASSWORD: self.config_entry.data.get(CONF_PASSWORD), + } + try: + api = await get_api(self.hass, **config) + except CannotConnect: + raise ConfigEntryNotReady + except (AuthenticationError, UnknownError): + return False + + self.tm_data = self.hass.data[DOMAIN][DATA_TRANSMISSION] = TransmissionData( + self.hass, self.config_entry, api + ) + + await self.hass.async_add_executor_job(self.tm_data.init_torrent_list) + await self.hass.async_add_executor_job(self.tm_data.update) + self.set_scan_interval(self.scan_interval) - def add_torrent(service): - """Add new torrent to download.""" - torrent = service.data[ATTR_TORRENT] - if torrent.startswith( - ("http", "ftp:", "magnet:") - ) or hass.config.is_allowed_path(torrent): - api.add_torrent(torrent) - else: - _LOGGER.warning( - "Could not add torrent: " "unsupported type or no permission" + for platform in ["sensor", "switch"]: + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, platform + ) ) - hass.services.register( - DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA - ) + def add_torrent(service): + """Add new torrent to download.""" + torrent = service.data[ATTR_TORRENT] + if torrent.startswith( + ("http", "ftp:", "magnet:") + ) or self.hass.config.is_allowed_path(torrent): + api.add_torrent(torrent) + else: + _LOGGER.warning( + "Could not add torrent: unsupported type or no permission" + ) + + self.hass.services.async_register( + DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA + ) + + self.config_entry.add_update_listener(self.async_options_updated) - sensorconfig = { - "sensors": config[DOMAIN][CONF_MONITORED_CONDITIONS], - "client_name": config[DOMAIN][CONF_NAME], - } + return True - discovery.load_platform(hass, "sensor", DOMAIN, sensorconfig, config) + def set_scan_interval(self, scan_interval): + """Update scan interval.""" - if config[DOMAIN][TURTLE_MODE]: - discovery.load_platform(hass, "switch", DOMAIN, sensorconfig, config) + def refresh(event_time): + """Get the latest data from Transmission.""" + self.tm_data.update() - return True + if self.unsub_timer is not None: + self.unsub_timer() + self.unsub_timer = async_track_time_interval( + self.hass, refresh, timedelta(seconds=scan_interval) + ) + + @staticmethod + async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + hass.data[DOMAIN][entry.entry_id].set_scan_interval( + entry.options[CONF_SCAN_INTERVAL] + ) class TransmissionData: @@ -133,6 +214,7 @@ class TransmissionData: def __init__(self, hass, config, api): """Initialize the Transmission RPC API.""" + self.hass = hass self.data = None self.torrents = None self.session = None @@ -140,12 +222,9 @@ def __init__(self, hass, config, api): self._api = api self.completed_torrents = [] self.started_torrents = [] - self.hass = hass def update(self): """Get the latest data from Transmission instance.""" - from transmissionrpc.error import TransmissionError - try: self.data = self._api.session_stats() self.torrents = self._api.get_torrents() @@ -153,15 +232,15 @@ def update(self): self.check_completed_torrent() self.check_started_torrent() + _LOGGER.debug("Torrent Data Updated") - dispatcher_send(self.hass, DATA_UPDATED) - - _LOGGER.debug("Torrent Data updated") self.available = True except TransmissionError: self.available = False _LOGGER.error("Unable to connect to Transmission client") + dispatcher_send(self.hass, DATA_UPDATED) + def init_torrent_list(self): """Initialize torrent lists.""" self.torrents = self._api.get_torrents() @@ -211,6 +290,15 @@ def get_completed_torrent_count(self): """Get the number of completed torrents.""" return len(self.completed_torrents) + def start_torrents(self): + """Start all torrents.""" + self._api.start_all() + + def stop_torrents(self): + """Stop all active torrents.""" + torrent_ids = [torrent.id for torrent in self.torrents] + self._api.stop_torrent(torrent_ids) + def set_alt_speed_enabled(self, is_enabled): """Set the alternative speed flag.""" self._api.set_session(alt_speed_enabled=is_enabled) diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py new file mode 100644 index 00000000000000..99376f4b6e0a87 --- /dev/null +++ b/homeassistant/components/transmission/config_flow.py @@ -0,0 +1,104 @@ +"""Config flow for Transmission Bittorent Client.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +from homeassistant.core import callback + +from . import get_api +from .const import DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN +from .errors import AuthenticationError, CannotConnect, UnknownError + + +class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a UniFi config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return TransmissionOptionsFlowHandler(config_entry) + + def __init__(self): + """Initialize the Transmission flow.""" + self.config = {} + self.errors = {} + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="one_instance_allowed") + + if user_input is not None: + + self.config[CONF_NAME] = user_input.pop(CONF_NAME) + try: + await get_api(self.hass, **user_input) + self.config.update(user_input) + if "options" not in self.config: + self.config["options"] = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} + return self.async_create_entry( + title=self.config[CONF_NAME], data=self.config + ) + except AuthenticationError: + self.errors[CONF_USERNAME] = "wrong_credentials" + self.errors[CONF_PASSWORD] = "wrong_credentials" + except (CannotConnect, UnknownError): + self.errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_HOST): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + ), + errors=self.errors, + ) + + async def async_step_import(self, import_config): + """Import from Transmission client config.""" + self.config["options"] = { + CONF_SCAN_INTERVAL: import_config.pop(CONF_SCAN_INTERVAL).seconds + } + + return await self.async_step_user(user_input=import_config) + + +class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Transmission client options.""" + + def __init__(self, config_entry): + """Initialize Transmission options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the Transmission options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, + self.config_entry.data["options"][CONF_SCAN_INTERVAL], + ), + ): int + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py new file mode 100644 index 00000000000000..e4a8b1490c28bf --- /dev/null +++ b/homeassistant/components/transmission/const.py @@ -0,0 +1,24 @@ +"""Constants for the Transmission Bittorent Client component.""" +DOMAIN = "transmission" + +SENSOR_TYPES = { + "active_torrents": ["Active Torrents", None], + "current_status": ["Status", None], + "download_speed": ["Down Speed", "MB/s"], + "paused_torrents": ["Paused Torrents", None], + "total_torrents": ["Total Torrents", None], + "upload_speed": ["Up Speed", "MB/s"], + "completed_torrents": ["Completed Torrents", None], + "started_torrents": ["Started Torrents", None], +} +SWITCH_TYPES = {"on_off": "Switch", "turtle_mode": "Turtle Mode"} + +DEFAULT_NAME = "Transmission" +DEFAULT_PORT = 9091 +DEFAULT_SCAN_INTERVAL = 120 + +ATTR_TORRENT = "torrent" +SERVICE_ADD_TORRENT = "add_torrent" + +DATA_UPDATED = "transmission_data_updated" +DATA_TRANSMISSION = "data_transmission" diff --git a/homeassistant/components/transmission/errors.py b/homeassistant/components/transmission/errors.py new file mode 100644 index 00000000000000..b5f74f7bf4066c --- /dev/null +++ b/homeassistant/components/transmission/errors.py @@ -0,0 +1,14 @@ +"""Errors for the Transmission component.""" +from homeassistant.exceptions import HomeAssistantError + + +class AuthenticationError(HomeAssistantError): + """Wrong Username or Password.""" + + +class CannotConnect(HomeAssistantError): + """Unable to connect to client.""" + + +class UnknownError(HomeAssistantError): + """Unknown Error.""" diff --git a/homeassistant/components/transmission/manifest.json b/homeassistant/components/transmission/manifest.json index bc5da64fcacd9b..2bd4571ef93b92 100644 --- a/homeassistant/components/transmission/manifest.json +++ b/homeassistant/components/transmission/manifest.json @@ -1,10 +1,13 @@ { "domain": "transmission", "name": "Transmission", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/transmission", "requirements": [ "transmissionrpc==0.11" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@engrbm87" + ] +} \ No newline at end of file diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index ac2e64ce92f390..30dfa4a3cbed47 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -1,32 +1,29 @@ """Support for monitoring the Transmission BitTorrent client API.""" -from datetime import timedelta import logging -from homeassistant.const import STATE_IDLE +from homeassistant.const import CONF_NAME, STATE_IDLE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import DATA_TRANSMISSION, DATA_UPDATED, SENSOR_TYPES +from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Transmission" -SCAN_INTERVAL = timedelta(seconds=120) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import config from configuration.yaml.""" + pass -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission sensors.""" - if discovery_info is None: - return - transmission_api = hass.data[DATA_TRANSMISSION] - monitored_variables = discovery_info["sensors"] - name = discovery_info["client_name"] + transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION] + name = config_entry.data[CONF_NAME] dev = [] - for sensor_type in monitored_variables: + for sensor_type in SENSOR_TYPES: dev.append( TransmissionSensor( sensor_type, diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index e049f89b3c6a6d..ab383584e83fdc 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -3,4 +3,4 @@ add_torrent: fields: torrent: description: URL, magnet link or Base64 encoded file. - example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent} + example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json new file mode 100644 index 00000000000000..7160cd109c4794 --- /dev/null +++ b/homeassistant/components/transmission/strings.json @@ -0,0 +1,40 @@ +{ + "config": { + "title": "Transmission", + "step": { + "user": { + "title": "Setup Transmission Client", + "data": { + "name": "Name", + "host": "Host", + "username": "User name", + "password": "Password", + "port": "Port" + } + }, + "options": { + "title": "Configure Options", + "data": { + "scan_interval": "Update frequency" + } + } + }, + "error": { + "wrong_credentials": "Wrong username or password", + "cannot_connect": "Unable to Connect to host" + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for Transmission", + "data": { + "scan_interval": "Update frequency" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index df490cdbe47cbe..0bb43f715aca37 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -1,43 +1,50 @@ """Support for setting the Transmission BitTorrent client Turtle Mode.""" import logging -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import ToggleEntity -from . import DATA_TRANSMISSION, DATA_UPDATED +from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SWITCH_TYPES _LOGGING = logging.getLogger(__name__) -DEFAULT_NAME = "Transmission Turtle Mode" - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import config from configuration.yaml.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission switch.""" - if discovery_info is None: - return - component_name = DATA_TRANSMISSION - transmission_api = hass.data[component_name] - name = discovery_info["client_name"] + transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION] + name = config_entry.data[CONF_NAME] - async_add_entities([TransmissionSwitch(transmission_api, name)], True) + dev = [] + for switch_type, switch_name in SWITCH_TYPES.items(): + dev.append(TransmissionSwitch(switch_type, switch_name, transmission_api, name)) + + async_add_entities(dev, True) class TransmissionSwitch(ToggleEntity): """Representation of a Transmission switch.""" - def __init__(self, transmission_client, name): + def __init__(self, switch_type, switch_name, transmission_api, name): """Initialize the Transmission switch.""" - self._name = name - self.transmission_client = transmission_client + self._name = switch_name + self.client_name = name + self.type = switch_type + self._transmission_api = transmission_api self._state = STATE_OFF + self._data = None @property def name(self): """Return the name of the switch.""" - return self._name + return f"{self.client_name} {self._name}" @property def state(self): @@ -54,15 +61,30 @@ def is_on(self): """Return true if device is on.""" return self._state == STATE_ON + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self._transmission_api.available + def turn_on(self, **kwargs): """Turn the device on.""" - _LOGGING.debug("Turning Turtle Mode of Transmission on") - self.transmission_client.set_alt_speed_enabled(True) + if self.type == "on_off": + _LOGGING.debug("Starting all torrents") + self._transmission_api.start_torrents() + elif self.type == "turtle_mode": + _LOGGING.debug("Turning Turtle Mode of Transmission on") + self._transmission_api.set_alt_speed_enabled(True) + self._transmission_api.update() def turn_off(self, **kwargs): """Turn the device off.""" - _LOGGING.debug("Turning Turtle Mode of Transmission off") - self.transmission_client.set_alt_speed_enabled(False) + if self.type == "on_off": + _LOGGING.debug("Stoping all torrents") + self._transmission_api.stop_torrents() + if self.type == "turtle_mode": + _LOGGING.debug("Turning Turtle Mode of Transmission off") + self._transmission_api.set_alt_speed_enabled(False) + self._transmission_api.update() async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -76,7 +98,14 @@ def _schedule_immediate_update(self): def update(self): """Get the latest data from Transmission and updates the state.""" - active = self.transmission_client.get_alt_speed_enabled() + active = None + if self.type == "on_off": + self._data = self._transmission_api.data + if self._data: + active = self._data.activeTorrentCount > 0 + + elif self.type == "turtle_mode": + active = self._transmission_api.get_alt_speed_enabled() if active is None: return diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b6865f9e86a16e..ab7b339e58284a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -62,6 +62,7 @@ "tplink", "traccar", "tradfri", + "transmission", "twentemilieu", "twilio", "unifi", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d42120c339ea27..d790a423de3888 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -422,6 +422,9 @@ statsd==3.2.1 # homeassistant.components.toon toonapilib==3.2.4 +# homeassistant.components.transmission +transmissionrpc==0.11 + # homeassistant.components.twentemilieu twentemilieu==0.1.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fcb265bbc97b6b..1e484e0dfc4853 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -173,6 +173,7 @@ "srpenergy", "statsd", "toonapilib", + "transmissionrpc", "twentemilieu", "uvcclient", "vsure", diff --git a/tests/components/transmission/__init__.py b/tests/components/transmission/__init__.py new file mode 100644 index 00000000000000..b8f8d8c847fa50 --- /dev/null +++ b/tests/components/transmission/__init__.py @@ -0,0 +1 @@ +"""Tests for Transmission.""" diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py new file mode 100644 index 00000000000000..e79f5c8ac96a88 --- /dev/null +++ b/tests/components/transmission/test_config_flow.py @@ -0,0 +1,245 @@ +"""Tests for Met.no config flow.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest +from transmissionrpc.error import TransmissionError + +from homeassistant import data_entry_flow +from homeassistant.components.transmission import config_flow +from homeassistant.components.transmission.const import ( + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) + +from tests.common import MockConfigEntry + +NAME = "Transmission" +HOST = "192.168.1.100" +USERNAME = "username" +PASSWORD = "password" +PORT = 9091 +SCAN_INTERVAL = 10 + + +@pytest.fixture(name="api") +def mock_transmission_api(): + """Mock an api.""" + with patch("transmissionrpc.Client"): + yield + + +@pytest.fixture(name="auth_error") +def mock_api_authentication_error(): + """Mock an api.""" + with patch( + "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") + ): + yield + + +@pytest.fixture(name="conn_error") +def mock_api_connection_error(): + """Mock an api.""" + with patch( + "transmissionrpc.Client", + side_effect=TransmissionError("111: Connection refused"), + ): + yield + + +@pytest.fixture(name="unknown_error") +def mock_api_unknown_error(): + """Mock an api.""" + with patch("transmissionrpc.Client", side_effect=TransmissionError): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.TransmissionFlowHandler() + flow.hass = hass + return flow + + +async def test_flow_works(hass, api): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # test with required fields only + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + + # test with all provided + result = await flow.async_step_user( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + } + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_PORT] == PORT + assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + + +async def test_options(hass): + """Test updating options.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=CONF_NAME, + data={ + "name": DEFAULT_NAME, + "host": HOST, + "username": USERNAME, + "password": PASSWORD, + "port": DEFAULT_PORT, + "options": {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + }, + options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + ) + flow = init_config_flow(hass) + options_flow = flow.async_get_options_flow(entry) + + result = await options_flow.async_step_init() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_SCAN_INTERVAL] == 10 + + +async def test_import(hass, api): + """Test import step.""" + flow = init_config_flow(hass) + + # import with minimum fields only + result = await flow.async_step_import( + { + CONF_NAME: DEFAULT_NAME, + CONF_HOST: HOST, + CONF_PORT: DEFAULT_PORT, + CONF_SCAN_INTERVAL: timedelta(seconds=DEFAULT_SCAN_INTERVAL), + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"][CONF_NAME] == DEFAULT_NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == DEFAULT_PORT + assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + + # import with all + result = await flow.async_step_import( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + CONF_SCAN_INTERVAL: timedelta(seconds=SCAN_INTERVAL), + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_PORT] == PORT + assert result["data"]["options"][CONF_SCAN_INTERVAL] == SCAN_INTERVAL + + +async def test_integration_already_exists(hass, api): + """Test we only allow a single config flow.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "abort" + assert result["reason"] == "one_instance_allowed" + + +async def test_error_on_wrong_credentials(hass, auth_error): + """Test with wrong credentials.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == { + CONF_USERNAME: "wrong_credentials", + CONF_PASSWORD: "wrong_credentials", + } + + +async def test_error_on_connection_failure(hass, conn_error): + """Test when connection to host fails.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_error_on_unknwon_error(hass, unknown_error): + """Test when connection to host fails.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} From 3efdf29dfa178838002201eaea258db78e6fa2ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Vran=C3=ADk?= Date: Thu, 26 Sep 2019 11:24:03 +0200 Subject: [PATCH 172/296] Centralize rainbird config and add binary sensor platform (#26393) * Update pyrainbird to version 0.2.0 to fix zone number issue: - home-assistant/home-assistant/issues/24519 - jbarrancos/pyrainbird/issues/5 - https://community.home-assistant.io/t/rainbird-zone-switches-5-8-dont-correspond/104705 * requirements_all.txt regenerated * code formatting * pyrainbird version 0.3.0 * zone id * rainsensor return state * updating rainsensor * new version of pyrainbird * binary sensor state * quiet in check format * is_on instead of state for binary_sensor * no unit of measurement for binary sensor * no monitored conditions config * get keys of dict directly * removed redundant update of state * simplified switch * right states for switch * raindelay sensor * raindelay sensor * binary sensor state * binary sensor state * reorganized imports * doc on public method * reformatted * add irrigation service to rain bird, which allows you to set the duration * rebased on konikvranik and solved some feedback * add irrigation service to rain bird * sensor types to constants * synchronized register service * patform discovery * binary sensor as wrapper to sensor * version 0.4.0 * new config approach * sensors cleanup * bypass if no zones found * platform schema removed * Change config schema to list of controllers some small code improvements as suggested in CR: - dictionary acces by [] - just return instead of return False - import order - no optional parameter name * some small code improvements as suggested in CR: - supported platforms in constant - just return instead of return False - removed unused constant * No single controller configuration Co-Authored-By: Martin Hjelmare * pyrainbird 0.4.1 * individual switch configuration * imports order * generate default name out of entity * trigger time required for controller * incorporated CR remarks: - constant fo rzones - removed SCAN_INTERVAL - detection of success on initialization - removed underscore - refactored if/else - empty line on end of file - hass as first parameter * import of library on top * refactored * Update homeassistant/components/rainbird/__init__.py Co-Authored-By: Martin Hjelmare * validate time and set defaults * set defaults on right place * pylint bypass * iterate over values * codeowner * reverted changes: * irrigation time just as positive integer. Making it complex does make sense * zone edfaults fullfiled at runtime. There is no information about available zones in configuration time. * codeowners updated * accept timedelta in irrigation time * simplified time calculation * call total_seconds * irrigation time as seconds. * simplified schema --- CODEOWNERS | 1 + homeassistant/components/rainbird/__init__.py | 83 +++++++++++--- .../components/rainbird/binary_sensor.py | 64 +++++++++++ .../components/rainbird/manifest.json | 6 +- homeassistant/components/rainbird/sensor.py | 47 ++++---- .../components/rainbird/services.yaml | 9 ++ homeassistant/components/rainbird/switch.py | 105 ++++++++++-------- requirements_all.txt | 2 +- 8 files changed, 225 insertions(+), 92 deletions(-) create mode 100644 homeassistant/components/rainbird/binary_sensor.py create mode 100644 homeassistant/components/rainbird/services.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 11b99b42a44ac1..419bc1a8606abb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -226,6 +226,7 @@ homeassistant/components/qld_bushfire/* @exxamalte homeassistant/components/qnap/* @colinodell homeassistant/components/quantum_gateway/* @cisasteelersfan homeassistant/components/qwikswitch/* @kellerza +homeassistant/components/rainbird/* @konikvranik homeassistant/components/raincloud/* @vanstinator homeassistant/components/rainforest_eagle/* @gtdiehl homeassistant/components/rainmachine/* @bachya diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 1d8ed8e37b169c..0b51be1f258018 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -1,42 +1,91 @@ """Support for Rain Bird Irrigation system LNK WiFi Module.""" import logging +from pyrainbird import RainbirdController import voluptuous as vol +from homeassistant.components import binary_sensor, sensor, switch +from homeassistant.const import ( + CONF_FRIENDLY_NAME, + CONF_HOST, + CONF_PASSWORD, + CONF_TRIGGER_TIME, +) +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD + +CONF_ZONES = "zones" + +SUPPORTED_PLATFORMS = [switch.DOMAIN, sensor.DOMAIN, binary_sensor.DOMAIN] _LOGGER = logging.getLogger(__name__) +RAINBIRD_CONTROLLER = "controller" DATA_RAINBIRD = "rainbird" DOMAIN = "rainbird" -CONFIG_SCHEMA = vol.Schema( +SENSOR_TYPE_RAINDELAY = "raindelay" +SENSOR_TYPE_RAINSENSOR = "rainsensor" +# sensor_type [ description, unit, icon ] +SENSOR_TYPES = { + SENSOR_TYPE_RAINSENSOR: ["Rainsensor", None, "mdi:water"], + SENSOR_TYPE_RAINDELAY: ["Raindelay", None, "mdi:water-off"], +} + +TRIGGER_TIME_SCHEMA = vol.All( + cv.time_period, cv.positive_timedelta, lambda td: (td.total_seconds() // 60) +) + +ZONE_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string} - ) - }, + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_TRIGGER_TIME): TRIGGER_TIME_SCHEMA, + } +) +CONTROLLER_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_TRIGGER_TIME): TRIGGER_TIME_SCHEMA, + vol.Optional(CONF_ZONES): vol.Schema({cv.positive_int: ZONE_SCHEMA}), + } +) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CONTROLLER_SCHEMA]))}, extra=vol.ALLOW_EXTRA, ) def setup(hass, config): """Set up the Rain Bird component.""" - conf = config[DOMAIN] - server = conf.get(CONF_HOST) - password = conf.get(CONF_PASSWORD) - from pyrainbird import RainbirdController + hass.data[DATA_RAINBIRD] = [] + success = False + for controller_config in config[DOMAIN]: + success = success or _setup_controller(hass, controller_config, config) - controller = RainbirdController(server, password) + return success - _LOGGER.debug("Rain Bird Controller set to: %s", server) - initial_status = controller.currentIrrigation() - if initial_status and initial_status["type"] != "CurrentStationsActiveResponse": - _LOGGER.error("Error getting state. Possible configuration issues") +def _setup_controller(hass, controller_config, config): + """Set up a controller.""" + server = controller_config[CONF_HOST] + password = controller_config[CONF_PASSWORD] + controller = RainbirdController(server, password) + position = len(hass.data[DATA_RAINBIRD]) + try: + controller.get_serial_number() + except Exception as exc: # pylint: disable=W0703 + _LOGGER.error("Unable to setup controller: %s", exc) return False - - hass.data[DATA_RAINBIRD] = controller + hass.data[DATA_RAINBIRD].append(controller) + _LOGGER.debug("Rain Bird Controller %d set to: %s", position, server) + for platform in SUPPORTED_PLATFORMS: + discovery.load_platform( + hass, + platform, + DOMAIN, + {RAINBIRD_CONTROLLER: position, **controller_config}, + config, + ) return True diff --git a/homeassistant/components/rainbird/binary_sensor.py b/homeassistant/components/rainbird/binary_sensor.py new file mode 100644 index 00000000000000..51c5f7a9dbefe7 --- /dev/null +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -0,0 +1,64 @@ +"""Support for Rain Bird Irrigation system LNK WiFi Module.""" +import logging + +from pyrainbird import RainbirdController + +from homeassistant.components.binary_sensor import BinarySensorDevice + +from . import ( + DATA_RAINBIRD, + RAINBIRD_CONTROLLER, + SENSOR_TYPE_RAINDELAY, + SENSOR_TYPE_RAINSENSOR, + SENSOR_TYPES, +) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a Rain Bird sensor.""" + if discovery_info is None: + return + + controller = hass.data[DATA_RAINBIRD][discovery_info[RAINBIRD_CONTROLLER]] + add_entities( + [RainBirdSensor(controller, sensor_type) for sensor_type in SENSOR_TYPES], True + ) + + +class RainBirdSensor(BinarySensorDevice): + """A sensor implementation for Rain Bird device.""" + + def __init__(self, controller: RainbirdController, sensor_type): + """Initialize the Rain Bird sensor.""" + self._sensor_type = sensor_type + self._controller = controller + self._name = SENSOR_TYPES[self._sensor_type][0] + self._icon = SENSOR_TYPES[self._sensor_type][2] + self._state = None + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return None if self._state is None else bool(self._state) + + def update(self): + """Get the latest data and updates the states.""" + _LOGGER.debug("Updating sensor: %s", self._name) + state = None + if self._sensor_type == SENSOR_TYPE_RAINSENSOR: + state = self._controller.get_rain_sensor_state() + elif self._sensor_type == SENSOR_TYPE_RAINDELAY: + state = self._controller.get_rain_delay() + self._state = None if state is None else bool(state) + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def icon(self): + """Return icon.""" + return self._icon diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 584ea22afe23ef..b911aaa57e19b5 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -3,8 +3,10 @@ "name": "Rainbird", "documentation": "https://www.home-assistant.io/components/rainbird", "requirements": [ - "pyrainbird==0.2.1" + "pyrainbird==0.4.1" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@konikvranik" + ] } diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index 2d4549a21d5dba..501566de6823bc 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -1,44 +1,37 @@ """Support for Rain Bird Irrigation system LNK WiFi Module.""" import logging -import voluptuous as vol +from pyrainbird import RainbirdController -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_MONITORED_CONDITIONS -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from . import DATA_RAINBIRD +from . import ( + DATA_RAINBIRD, + RAINBIRD_CONTROLLER, + SENSOR_TYPE_RAINDELAY, + SENSOR_TYPE_RAINSENSOR, + SENSOR_TYPES, +) _LOGGER = logging.getLogger(__name__) -# sensor_type [ description, unit, icon ] -SENSOR_TYPES = {"rainsensor": ["Rainsensor", None, "mdi:water"]} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ) - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Rain Bird sensor.""" - controller = hass.data[DATA_RAINBIRD] - sensors = [] - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - sensors.append(RainBirdSensor(controller, sensor_type)) + if discovery_info is None: + return - add_entities(sensors, True) + controller = hass.data[DATA_RAINBIRD][discovery_info[RAINBIRD_CONTROLLER]] + add_entities( + [RainBirdSensor(controller, sensor_type) for sensor_type in SENSOR_TYPES], True + ) class RainBirdSensor(Entity): """A sensor implementation for Rain Bird device.""" - def __init__(self, controller, sensor_type): + def __init__(self, controller: RainbirdController, sensor_type): """Initialize the Rain Bird sensor.""" self._sensor_type = sensor_type self._controller = controller @@ -55,12 +48,10 @@ def state(self): def update(self): """Get the latest data and updates the states.""" _LOGGER.debug("Updating sensor: %s", self._name) - if self._sensor_type == "rainsensor": - result = self._controller.currentRainSensorState() - if result and result["type"] == "CurrentRainSensorStateResponse": - self._state = result["sensorState"] - else: - self._state = None + if self._sensor_type == SENSOR_TYPE_RAINSENSOR: + self._state = self._controller.get_rain_sensor_state() + elif self._sensor_type == SENSOR_TYPE_RAINDELAY: + self._state = self._controller.get_rain_delay() @property def name(self): diff --git a/homeassistant/components/rainbird/services.yaml b/homeassistant/components/rainbird/services.yaml new file mode 100644 index 00000000000000..cdac7171a25c8e --- /dev/null +++ b/homeassistant/components/rainbird/services.yaml @@ -0,0 +1,9 @@ +start_irrigation: + description: Start the irrigation + fields: + entity_id: + description: Name of a single irrigation to turn on + example: 'switch.sprinkler_1' + duration: + description: Duration for this sprinkler to be turned on + example: 1 diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 868e8ff4c7d65b..cb4ac83090f127 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -2,61 +2,85 @@ import logging +from pyrainbird import AvailableStations, RainbirdController import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import ( - CONF_FRIENDLY_NAME, - CONF_SCAN_INTERVAL, - CONF_SWITCHES, - CONF_TRIGGER_TIME, - CONF_ZONE, -) +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import ATTR_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_TRIGGER_TIME from homeassistant.helpers import config_validation as cv -from . import DATA_RAINBIRD +from . import CONF_ZONES, DATA_RAINBIRD, DOMAIN, RAINBIRD_CONTROLLER -DOMAIN = "rainbird" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +ATTR_DURATION = "duration" + +SERVICE_START_IRRIGATION = "start_irrigation" + +SERVICE_SCHEMA_IRRIGATION = vol.Schema( { - vol.Required(CONF_SWITCHES, default={}): vol.Schema( - { - cv.string: { - vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_ZONE): cv.string, - vol.Required(CONF_TRIGGER_TIME): cv.string, - vol.Optional(CONF_SCAN_INTERVAL): cv.string, - } - } - ) + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_DURATION): vol.All(vol.Coerce(float), vol.Range(min=0)), } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Rain Bird switches over a Rain Bird controller.""" - controller = hass.data[DATA_RAINBIRD] + if discovery_info is None: + return + + controller: RainbirdController = hass.data[DATA_RAINBIRD][ + discovery_info[RAINBIRD_CONTROLLER] + ] + available_stations: AvailableStations = controller.get_available_stations() + if not (available_stations and available_stations.stations): + return devices = [] - for dev_id, switch in config.get(CONF_SWITCHES).items(): - devices.append(RainBirdSwitch(controller, switch, dev_id)) + for zone in range(1, available_stations.stations.count + 1): + if available_stations.stations.active(zone): + zone_config = discovery_info.get(CONF_ZONES, {}).get(zone, {}) + time = zone_config.get(CONF_TRIGGER_TIME, discovery_info[CONF_TRIGGER_TIME]) + name = zone_config.get(CONF_FRIENDLY_NAME) + devices.append( + RainBirdSwitch( + controller, + zone, + time, + name if name else "Sprinkler {}".format(zone), + ) + ) + add_entities(devices, True) + def start_irrigation(service): + entity_id = service.data[ATTR_ENTITY_ID] + duration = service.data[ATTR_DURATION] + + for device in devices: + if device.entity_id == entity_id: + device.turn_on(duration=duration) + + hass.services.register( + DOMAIN, + SERVICE_START_IRRIGATION, + start_irrigation, + schema=SERVICE_SCHEMA_IRRIGATION, + ) + class RainBirdSwitch(SwitchDevice): """Representation of a Rain Bird switch.""" - def __init__(self, rb, dev, dev_id): + def __init__(self, controller: RainbirdController, zone, time, name): """Initialize a Rain Bird Switch Device.""" - self._rainbird = rb - self._devid = dev_id - self._zone = int(dev.get(CONF_ZONE)) - self._name = dev.get(CONF_FRIENDLY_NAME, f"Sprinkler {self._zone}") + self._rainbird = controller + self._zone = zone + self._name = name self._state = None - self._duration = dev.get(CONF_TRIGGER_TIME) - self._attributes = {"duration": self._duration, "zone": self._zone} + self._duration = time + self._attributes = {ATTR_DURATION: self._duration, "zone": self._zone} @property def device_state_attributes(self): @@ -70,27 +94,20 @@ def name(self): def turn_on(self, **kwargs): """Turn the switch on.""" - response = self._rainbird.startIrrigation(int(self._zone), int(self._duration)) - if response and response["type"] == "AcknowledgeResponse": + if self._rainbird.irrigate_zone( + int(self._zone), + int(kwargs[ATTR_DURATION] if ATTR_DURATION in kwargs else self._duration), + ): self._state = True def turn_off(self, **kwargs): """Turn the switch off.""" - response = self._rainbird.stopIrrigation() - if response and response["type"] == "AcknowledgeResponse": + if self._rainbird.stop_irrigation(): self._state = False - def get_device_status(self): - """Get the status of the switch from Rain Bird Controller.""" - response = self._rainbird.currentIrrigation() - if response is None: - return None - if isinstance(response, dict) and "sprinklers" in response: - return response["sprinklers"][self._zone] - def update(self): """Update switch status.""" - self._state = self.get_device_status() + self._state = self._rainbird.get_zone_state(self._zone) @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 5bbd77f4d01b91..3b9eb718737bd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1396,7 +1396,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==0.2.1 +pyrainbird==0.4.1 # homeassistant.components.recswitch pyrecswitch==1.0.2 From c194f4a8137a2ca06cfc5545510c5fe552c4b546 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Thu, 26 Sep 2019 15:23:44 -0400 Subject: [PATCH 173/296] Add ecobee services to create and delete vacations (#26923) * Bump pyecobee to 0.1.3 * Add functions, attrs, and services to climate Fixes * Update requirements * Update service strings * Fix typo * Fix log msg string formatting for lint * Change some parameters to Inclusive start_date, start_time, end_date, and end_time must be specified together. * Use built-in convert util for temps * Match other service variable names --- homeassistant/components/ecobee/climate.py | 139 +++++++++++++++++- homeassistant/components/ecobee/manifest.json | 2 +- homeassistant/components/ecobee/services.yaml | 52 +++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 190 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 9eb8e8f26bc6ef..460bd2bb4a4ecf 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -33,12 +33,21 @@ ATTR_TEMPERATURE, TEMP_FAHRENHEIT, ) +from homeassistant.util.temperature import convert import homeassistant.helpers.config_validation as cv from .const import DOMAIN, _LOGGER +ATTR_COOL_TEMP = "cool_temp" +ATTR_END_DATE = "end_date" +ATTR_END_TIME = "end_time" ATTR_FAN_MIN_ON_TIME = "fan_min_on_time" +ATTR_FAN_MODE = "fan_mode" +ATTR_HEAT_TEMP = "heat_temp" ATTR_RESUME_ALL = "resume_all" +ATTR_START_DATE = "start_date" +ATTR_START_TIME = "start_time" +ATTR_VACATION_NAME = "vacation_name" DEFAULT_RESUME_ALL = False PRESET_TEMPERATURE = "temp" @@ -84,13 +93,37 @@ PRESET_HOLD_INDEFINITE: "indefinite", } -SERVICE_SET_FAN_MIN_ON_TIME = "set_fan_min_on_time" +SERVICE_CREATE_VACATION = "create_vacation" +SERVICE_DELETE_VACATION = "delete_vacation" SERVICE_RESUME_PROGRAM = "resume_program" +SERVICE_SET_FAN_MIN_ON_TIME = "set_fan_min_on_time" -SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema( +DTGROUP_INCLUSIVE_MSG = ( + f"{ATTR_START_DATE}, {ATTR_START_TIME}, {ATTR_END_DATE}, " + f"and {ATTR_END_TIME} must be specified together" +) + +CREATE_VACATION_SCHEMA = vol.Schema( { - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_VACATION_NAME): cv.string, + vol.Required(ATTR_COOL_TEMP): vol.Coerce(float), + vol.Required(ATTR_HEAT_TEMP): vol.Coerce(float), + vol.Inclusive(ATTR_START_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, + vol.Inclusive(ATTR_START_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, + vol.Inclusive(ATTR_END_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, + vol.Inclusive(ATTR_END_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, + vol.Optional(ATTR_FAN_MODE, default="auto"): vol.Any("auto", "on"), + vol.Optional(ATTR_FAN_MIN_ON_TIME, default=0): vol.All( + int, vol.Range(min=0, max=60) + ), + } +) + +DELETE_VACATION_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_VACATION_NAME): cv.string, } ) @@ -101,6 +134,14 @@ } ) +SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), + } +) + + SUPPORT_FLAGS = ( SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -124,6 +165,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(devices, True) + def create_vacation_service(service): + """Create a vacation on the target thermostat.""" + entity_id = service.data[ATTR_ENTITY_ID] + + for thermostat in devices: + if thermostat.entity_id == entity_id: + thermostat.create_vacation(service.data) + thermostat.schedule_update_ha_state(True) + break + + def delete_vacation_service(service): + """Delete a vacation on the target thermostat.""" + entity_id = service.data[ATTR_ENTITY_ID] + vacation_name = service.data[ATTR_VACATION_NAME] + + for thermostat in devices: + if thermostat.entity_id == entity_id: + thermostat.delete_vacation(vacation_name) + thermostat.schedule_update_ha_state(True) + break + def fan_min_on_time_set_service(service): """Set the minimum fan on time on the target thermostats.""" entity_id = service.data.get(ATTR_ENTITY_ID) @@ -158,6 +220,20 @@ def resume_program_set_service(service): thermostat.schedule_update_ha_state(True) + hass.services.async_register( + DOMAIN, + SERVICE_CREATE_VACATION, + create_vacation_service, + schema=CREATE_VACATION_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_DELETE_VACATION, + delete_vacation_service, + schema=DELETE_VACATION_SCHEMA, + ) + hass.services.async_register( DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, @@ -536,3 +612,58 @@ def hold_preference(self): # supported; note that this should not include 'indefinite' # as an indefinite away hold is interpreted as away_mode return "nextTransition" + + def create_vacation(self, service_data): + """Create a vacation with user-specified parameters.""" + vacation_name = service_data[ATTR_VACATION_NAME] + cool_temp = convert( + service_data[ATTR_COOL_TEMP], + self.hass.config.units.temperature_unit, + TEMP_FAHRENHEIT, + ) + heat_temp = convert( + service_data[ATTR_HEAT_TEMP], + self.hass.config.units.temperature_unit, + TEMP_FAHRENHEIT, + ) + start_date = service_data.get(ATTR_START_DATE) + start_time = service_data.get(ATTR_START_TIME) + end_date = service_data.get(ATTR_END_DATE) + end_time = service_data.get(ATTR_END_TIME) + fan_mode = service_data[ATTR_FAN_MODE] + fan_min_on_time = service_data[ATTR_FAN_MIN_ON_TIME] + + kwargs = { + key: value + for key, value in { + "start_date": start_date, + "start_time": start_time, + "end_date": end_date, + "end_time": end_time, + "fan_mode": fan_mode, + "fan_min_on_time": fan_min_on_time, + }.items() + if value is not None + } + + _LOGGER.debug( + "Creating a vacation on thermostat %s with name %s, cool temp %s, heat temp %s, " + "and the following other parameters: %s", + self.name, + vacation_name, + cool_temp, + heat_temp, + kwargs, + ) + self.data.ecobee.create_vacation( + self.thermostat_index, vacation_name, cool_temp, heat_temp, **kwargs + ) + + def delete_vacation(self, vacation_name): + """Delete a vacation with the specified name.""" + _LOGGER.debug( + "Deleting a vacation on thermostat %s with name %s", + self.name, + vacation_name, + ) + self.data.ecobee.delete_vacation(self.thermostat_index, vacation_name) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 092594c41fc0d4..131c35d7f89137 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/ecobee", "dependencies": [], - "requirements": ["python-ecobee-api==0.1.2"], + "requirements": ["python-ecobee-api==0.1.3"], "codeowners": ["@marthoc"] } diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml index 87eefed9eaaa43..2155d3cf7d24c3 100644 --- a/homeassistant/components/ecobee/services.yaml +++ b/homeassistant/components/ecobee/services.yaml @@ -1,3 +1,55 @@ +create_vacation: + description: >- + Create a vacation on the selected thermostat. Note: start/end date and time must all be specified + together for these parameters to have an effect. If start/end date and time are not specified, the + vacation will start immediately and last 14 days (unless deleted earlier). + fields: + entity_id: + description: ecobee thermostat on which to create the vacation (required). + example: "climate.kitchen" + vacation_name: + description: Name of the vacation to create; must be unique on the thermostat (required). + example: "Skiing" + cool_temp: + description: Cooling temperature during the vacation (required). + example: 23 + heat_temp: + description: Heating temperature during the vacation (required). + example: 25 + start_date: + description: >- + Date the vacation starts in the YYYY-MM-DD format (optional, immediately if not provided along with + start_time, end_date, and end_time). + example: "2019-03-15" + start_time: + description: Time the vacation starts, in the local time of the thermostat, in the 24-hour format "HH:MM:SS" + example: "20:00:00" + end_date: + description: >- + Date the vacation ends in the YYYY-MM-DD format (optional, 14 days from now if not provided along with + start_date, start_time, and end_time). + example: "2019-03-20" + end_time: + description: Time the vacation ends, in the local time of the thermostat, in the 24-hour format "HH:MM:SS" + example: "20:00:00" + fan_mode: + description: Fan mode of the thermostat during the vacation (auto or on) (optional, auto if not provided). + example: "on" + fan_min_on_time: + description: Minimum number of minutes to run the fan each hour (0 to 60) during the vacation (optional, 0 if not provided). + example: 30 + +delete_vacation: + description: >- + Delete a vacation on the selected thermostat. + fields: + entity_id: + description: ecobee thermostat on which to delete the vacation (required). + example: "climate.kitchen" + vacation_name: + description: Name of the vacation to delete (required). + example: "Skiing" + resume_program: description: Resume the programmed schedule. fields: diff --git a/requirements_all.txt b/requirements_all.txt index 3b9eb718737bd4..19bdc4efd833ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1483,7 +1483,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.1.2 +python-ecobee-api==0.1.3 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d790a423de3888..3a4fa60f15e697 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ pysonos==0.0.23 pyspcwebgw==0.4.0 # homeassistant.components.ecobee -python-ecobee-api==0.1.2 +python-ecobee-api==0.1.3 # homeassistant.components.darksky python-forecastio==1.4.0 From b04a70995ecc1a329801674fd45d8658f731e546 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 27 Sep 2019 00:32:12 +0000 Subject: [PATCH 174/296] [ci skip] Translation update --- .../components/adguard/.translations/da.json | 2 +- .../components/adguard/.translations/no.json | 2 +- .../components/adguard/.translations/sl.json | 2 +- .../ambiclimate/.translations/ca.json | 4 +- .../ambiclimate/.translations/ko.json | 2 +- .../ambiclimate/.translations/lb.json | 2 +- .../ambiclimate/.translations/pl.json | 2 +- .../ambiclimate/.translations/ru.json | 2 +- .../ambiclimate/.translations/sl.json | 2 +- .../ambiclimate/.translations/zh-Hant.json | 2 +- .../.translations/zh-Hant.json | 2 +- .../components/auth/.translations/es.json | 2 +- .../axis/.translations/zh-Hant.json | 14 +-- .../binary_sensor/.translations/zh-Hant.json | 92 +++++++++++++++++++ .../components/cast/.translations/no.json | 2 +- .../cast/.translations/zh-Hant.json | 2 +- .../daikin/.translations/zh-Hant.json | 6 +- .../components/deconz/.translations/cs.json | 2 +- .../components/deconz/.translations/es.json | 6 +- .../components/deconz/.translations/no.json | 2 +- .../deconz/.translations/zh-Hant.json | 8 +- .../dialogflow/.translations/cs.json | 2 +- .../dialogflow/.translations/fr.json | 2 +- .../dialogflow/.translations/no.json | 2 +- .../dialogflow/.translations/sl.json | 2 +- .../dialogflow/.translations/zh-Hant.json | 2 +- .../components/ecobee/.translations/da.json | 24 +++++ .../components/ecobee/.translations/no.json | 25 +++++ .../components/ecobee/.translations/ru.json | 25 +++++ .../components/ecobee/.translations/sv.json | 11 +++ .../ecobee/.translations/zh-Hant.json | 25 +++++ .../components/esphome/.translations/es.json | 2 +- .../components/geofency/.translations/no.json | 2 +- .../geofency/.translations/zh-Hant.json | 2 +- .../gpslogger/.translations/no.json | 2 +- .../gpslogger/.translations/zh-Hant.json | 2 +- .../components/heos/.translations/no.json | 2 +- .../heos/.translations/zh-Hant.json | 4 +- .../.translations/zh-Hant.json | 18 ++-- .../.translations/zh-Hant.json | 2 +- .../components/hue/.translations/zh-Hant.json | 2 +- .../components/ifttt/.translations/no.json | 2 +- .../components/ios/.translations/no.json | 2 +- .../components/izone/.translations/no.json | 2 +- .../components/lifx/.translations/no.json | 2 +- .../lifx/.translations/zh-Hant.json | 2 +- .../components/light/.translations/ca.json | 14 +-- .../components/light/.translations/da.json | 4 +- .../components/light/.translations/de.json | 4 +- .../components/light/.translations/hu.json | 17 ++++ .../components/light/.translations/nl.json | 10 +- .../components/light/.translations/pl.json | 6 +- .../components/locative/.translations/it.json | 2 +- .../components/locative/.translations/no.json | 2 +- .../locative/.translations/zh-Hant.json | 2 +- .../logi_circle/.translations/no.json | 2 +- .../logi_circle/.translations/zh-Hant.json | 2 +- .../components/mailgun/.translations/no.json | 2 +- .../mailgun/.translations/zh-Hant.json | 2 +- .../components/mqtt/.translations/no.json | 2 +- .../components/nest/.translations/no.json | 2 +- .../notion/.translations/zh-Hant.json | 2 +- .../owntracks/.translations/no.json | 2 +- .../owntracks/.translations/zh-Hant.json | 2 +- .../components/plaato/.translations/no.json | 2 +- .../plaato/.translations/zh-Hant.json | 2 +- .../components/plex/.translations/da.json | 11 +++ .../components/plex/.translations/en.json | 11 +++ .../components/plex/.translations/no.json | 10 ++ .../components/plex/.translations/ru.json | 11 +++ .../components/plex/.translations/sv.json | 11 +++ .../plex/.translations/zh-Hant.json | 14 ++- .../point/.translations/zh-Hant.json | 2 +- .../components/ps4/.translations/de.json | 2 +- .../components/ps4/.translations/zh-Hant.json | 8 +- .../smartthings/.translations/he.json | 2 +- .../somfy/.translations/zh-Hant.json | 2 +- .../components/sonos/.translations/no.json | 2 +- .../sonos/.translations/zh-Hant.json | 2 +- .../components/tplink/.translations/no.json | 2 +- .../tplink/.translations/zh-Hant.json | 2 +- .../components/traccar/.translations/de.json | 2 +- .../components/traccar/.translations/no.json | 2 +- .../traccar/.translations/zh-Hant.json | 2 +- .../transmission/.translations/da.json | 40 ++++++++ .../transmission/.translations/en.json | 42 ++++----- .../transmission/.translations/no.json | 40 ++++++++ .../transmission/.translations/ru.json | 40 ++++++++ .../transmission/.translations/sv.json | 37 ++++++++ .../components/twilio/.translations/no.json | 2 +- .../twilio/.translations/zh-Hant.json | 2 +- .../unifi/.translations/zh-Hant.json | 2 +- .../components/upnp/.translations/no.json | 2 +- .../upnp/.translations/zh-Hant.json | 4 +- .../components/wemo/.translations/no.json | 2 +- .../components/withings/.translations/sl.json | 2 +- .../withings/.translations/zh-Hant.json | 2 +- .../components/zha/.translations/no.json | 12 +-- .../components/zha/.translations/sv.json | 12 +++ .../components/zha/.translations/zh-Hant.json | 47 +++++++++- .../components/zwave/.translations/no.json | 2 +- 101 files changed, 648 insertions(+), 151 deletions(-) create mode 100644 homeassistant/components/binary_sensor/.translations/zh-Hant.json create mode 100644 homeassistant/components/ecobee/.translations/da.json create mode 100644 homeassistant/components/ecobee/.translations/no.json create mode 100644 homeassistant/components/ecobee/.translations/ru.json create mode 100644 homeassistant/components/ecobee/.translations/sv.json create mode 100644 homeassistant/components/ecobee/.translations/zh-Hant.json create mode 100644 homeassistant/components/light/.translations/hu.json create mode 100644 homeassistant/components/plex/.translations/sv.json create mode 100644 homeassistant/components/transmission/.translations/da.json create mode 100644 homeassistant/components/transmission/.translations/no.json create mode 100644 homeassistant/components/transmission/.translations/ru.json create mode 100644 homeassistant/components/transmission/.translations/sv.json diff --git a/homeassistant/components/adguard/.translations/da.json b/homeassistant/components/adguard/.translations/da.json index 0f854db0be6fe4..813405cec62471 100644 --- a/homeassistant/components/adguard/.translations/da.json +++ b/homeassistant/components/adguard/.translations/da.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Adguard Home, der leveres af Hass.io add-on: {addon}?", + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home, der leveres af Hass.io add-on: {addon}?", "title": "AdGuard Home via Hass.io add-on" }, "user": { diff --git a/homeassistant/components/adguard/.translations/no.json b/homeassistant/components/adguard/.translations/no.json index 94535d7e9450b1..2cd6cd72f6d3e7 100644 --- a/homeassistant/components/adguard/.translations/no.json +++ b/homeassistant/components/adguard/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "existing_instance_updated": "Oppdatert eksisterende konfigurasjon.", - "single_instance_allowed": "Kun \u00e9n enkelt konfigurasjon av AdGuard Hjemer tillatt." + "single_instance_allowed": "Kun en konfigurasjon av AdGuard Hjemer tillatt." }, "error": { "connection_error": "Tilkobling mislyktes." diff --git a/homeassistant/components/adguard/.translations/sl.json b/homeassistant/components/adguard/.translations/sl.json index 5c8d75d4cc8004..f1ca796363d270 100644 --- a/homeassistant/components/adguard/.translations/sl.json +++ b/homeassistant/components/adguard/.translations/sl.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja hass.io add-on {addon} ?", + "description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja Hass.io add-on {addon} ?", "title": "AdGuard Home preko dodatka Hass.io" }, "user": { diff --git a/homeassistant/components/ambiclimate/.translations/ca.json b/homeassistant/components/ambiclimate/.translations/ca.json index 054b1a89ae8af2..f446bf7390f976 100644 --- a/homeassistant/components/ambiclimate/.translations/ca.json +++ b/homeassistant/components/ambiclimate/.translations/ca.json @@ -3,7 +3,7 @@ "abort": { "access_token": "S'ha produ\u00eft un error desconegut al generat un testimoni d'acc\u00e9s.", "already_setup": "El compte d\u2019Ambi Climate est\u00e0 configurat.", - "no_config": "Necessites configurar Ambi Climate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Necessites configurar Ambiclimate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Ambi Climate." @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i Permet l'acc\u00e9s al teu compte de Ambi Climate, despr\u00e9s torna i prem Envia (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})", + "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i Permet l'acc\u00e9s al teu compte de Ambiclimate, despr\u00e9s torna i prem Envia (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})", "title": "Autenticaci\u00f3 amb Ambi Climate" } }, diff --git a/homeassistant/components/ambiclimate/.translations/ko.json b/homeassistant/components/ambiclimate/.translations/ko.json index be337bd3f0edfd..3b21726bcbeb13 100644 --- a/homeassistant/components/ambiclimate/.translations/ko.json +++ b/homeassistant/components/ambiclimate/.translations/ko.json @@ -3,7 +3,7 @@ "abort": { "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070 \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "already_setup": "Ambi Climate \uacc4\uc815\uc774 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "no_config": "Ambi Climate \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Ambi Climate \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/ambiclimate/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + "no_config": "Ambiclimate \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Ambiclimate \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/ambiclimate/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." }, "create_entry": { "default": "Ambi Climate \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/ambiclimate/.translations/lb.json b/homeassistant/components/ambiclimate/.translations/lb.json index a6ce441749d6d3..88be279ae7a260 100644 --- a/homeassistant/components/ambiclimate/.translations/lb.json +++ b/homeassistant/components/ambiclimate/.translations/lb.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Onbekannte Feeler beim gener\u00e9ieren vum Acc\u00e8s Jeton.", "already_setup": "Den Ambiclimate Kont ass konfigur\u00e9iert.", - "no_config": "Dir musst Ambiclimate konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/ambiclimatet/)." + "no_config": "Dir musst Ambiclimate konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Erfollegr\u00e4ich mat Ambiclimate authentifiz\u00e9iert." diff --git a/homeassistant/components/ambiclimate/.translations/pl.json b/homeassistant/components/ambiclimate/.translations/pl.json index 7ba95b007c995a..675c5e18776ca4 100644 --- a/homeassistant/components/ambiclimate/.translations/pl.json +++ b/homeassistant/components/ambiclimate/.translations/pl.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Nieznany b\u0142\u0105d podczas generowania tokena dost\u0119pu.", "already_setup": "Konto Ambiclimate jest skonfigurowane.", - "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 w nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 w nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119] (https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Ambiclimate" diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json index a4300e1e5306c5..5a816bce1401d5 100644 --- a/homeassistant/components/ambiclimate/.translations/ru.json +++ b/homeassistant/components/ambiclimate/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.", "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambiclimate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/ambiclimate/.translations/sl.json b/homeassistant/components/ambiclimate/.translations/sl.json index cae2e940d561b6..c5d84f030fa127 100644 --- a/homeassistant/components/ambiclimate/.translations/sl.json +++ b/homeassistant/components/ambiclimate/.translations/sl.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Neznana napaka pri ustvarjanju \u017eetona za dostop.", "already_setup": "Ra\u010dun Ambiclimate je konfiguriran.", - "no_config": "Ambiclimat morate konfigurirati, preden lahko z njo preverjate pristnost. [Preberite navodila] (https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Ambiclimate morate konfigurirati, preden lahko z njo preverjate pristnost. [Preberite navodila] (https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Uspe\u0161no overjeno z funkcijo Ambiclimate" diff --git a/homeassistant/components/ambiclimate/.translations/zh-Hant.json b/homeassistant/components/ambiclimate/.translations/zh-Hant.json index 28859cbf5912f7..1539429d0efc04 100644 --- a/homeassistant/components/ambiclimate/.translations/zh-Hant.json +++ b/homeassistant/components/ambiclimate/.translations/zh-Hant.json @@ -6,7 +6,7 @@ "no_config": "\u5fc5\u9808\u5148\u8a2d\u5b9a Ambiclimate \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/ambiclimate/\uff09\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Ambiclimate \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Ambiclimate \u8a2d\u5099\u3002" }, "error": { "follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002", diff --git a/homeassistant/components/ambient_station/.translations/zh-Hant.json b/homeassistant/components/ambient_station/.translations/zh-Hant.json index 7e3ed3ef888509..6c7c88a804576f 100644 --- a/homeassistant/components/ambient_station/.translations/zh-Hant.json +++ b/homeassistant/components/ambient_station/.translations/zh-Hant.json @@ -3,7 +3,7 @@ "error": { "identifier_exists": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u5df2\u8a3b\u518a", "invalid_key": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u7121\u6548", - "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u8a2d\u5099" }, "step": { "user": { diff --git a/homeassistant/components/auth/.translations/es.json b/homeassistant/components/auth/.translations/es.json index dd1d6f5437760b..5603c14fe1a774 100644 --- a/homeassistant/components/auth/.translations/es.json +++ b/homeassistant/components/auth/.translations/es.json @@ -13,7 +13,7 @@ "title": "Configure una contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n" }, "setup": { - "description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de ** notificar. {notify_service} **. Por favor introd\u00facela a continuaci\u00f3n:", + "description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de ** notify. {notify_service} **. Por favor introd\u00facela a continuaci\u00f3n:", "title": "Verificar la configuraci\u00f3n" } }, diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json index 1457db2b600276..c0d0df02135192 100644 --- a/homeassistant/components/axis/.translations/zh-Hant.json +++ b/homeassistant/components/axis/.translations/zh-Hant.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", - "not_axis_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Axis \u88dd\u7f6e" + "not_axis_device": "\u6240\u767c\u73fe\u7684\u8a2d\u5099\u4e26\u975e Axis \u8a2d\u5099" }, "error": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_in_progress": "\u88dd\u7f6e\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", - "device_unavailable": "\u88dd\u7f6e\u7121\u6cd5\u4f7f\u7528", + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "device_unavailable": "\u8a2d\u5099\u7121\u6cd5\u4f7f\u7528", "faulty_credentials": "\u4f7f\u7528\u8005\u6191\u8b49\u7121\u6548" }, "step": { @@ -20,9 +20,9 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "title": "\u8a2d\u5b9a Axis \u88dd\u7f6e" + "title": "\u8a2d\u5b9a Axis \u8a2d\u5099" } }, - "title": "Axis \u88dd\u7f6e" + "title": "Axis \u8a2d\u5099" } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/zh-Hant.json b/homeassistant/components/binary_sensor/.translations/zh-Hant.json new file mode 100644 index 00000000000000..36c72dcb9e69b4 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/zh-Hant.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \u96fb\u91cf\u904e\u4f4e", + "is_cold": "{entity_name} \u51b7", + "is_connected": "{entity_name} \u5df2\u9023\u7dda", + "is_gas": "{entity_name} \u5075\u6e2c\u5230\u6c23\u9ad4", + "is_hot": "{entity_name} \u71b1", + "is_light": "{entity_name} \u5075\u6e2c\u5230\u5149\u7dda\u4e2d", + "is_locked": "{entity_name} \u5df2\u4e0a\u9396", + "is_moist": "{entity_name} \u6f6e\u6fd5", + "is_motion": "{entity_name} \u5075\u6e2c\u5230\u52d5\u4f5c\u4e2d", + "is_moving": "{entity_name} \u79fb\u52d5\u4e2d", + "is_no_gas": "{entity_name} \u672a\u5075\u6e2c\u5230\u6c23\u9ad4", + "is_no_light": "{entity_name} \u672a\u5075\u6e2c\u5230\u5149\u7dda", + "is_no_motion": "{entity_name} \u672a\u5075\u6e2c\u5230\u52d5\u4f5c", + "is_no_problem": "{entity_name} \u672a\u5075\u6e2c\u5230\u554f\u984c", + "is_no_smoke": "{entity_name} \u672a\u5075\u6e2c\u5230\u7159\u9727", + "is_no_sound": "{entity_name} \u672a\u5075\u6e2c\u5230\u8072\u97f3", + "is_no_vibration": "{entity_name} \u672a\u5075\u6e2c\u5230\u9707\u52d5", + "is_not_bat_low": "{entity_name} \u96fb\u91cf\u6b63\u5e38", + "is_not_cold": "{entity_name} \u4e0d\u51b7", + "is_not_connected": "{entity_name} \u65b7\u7dda", + "is_not_hot": "{entity_name} \u4e0d\u71b1", + "is_not_locked": "{entity_name} \u89e3\u9396", + "is_not_moist": "{entity_name} \u4e7e\u71e5", + "is_not_moving": "{entity_name} \u672a\u5728\u79fb\u52d5", + "is_not_occupied": "{entity_name} \u672a\u6709\u4eba", + "is_not_open": "{entity_name} \u95dc\u9589", + "is_not_plugged_in": "{entity_name} \u672a\u63d2\u5165", + "is_not_powered": "{entity_name} \u672a\u901a\u96fb", + "is_not_present": "{entity_name} \u672a\u51fa\u73fe", + "is_not_unsafe": "{entity_name} \u5b89\u5168", + "is_occupied": "{entity_name} \u6709\u4eba", + "is_off": "{entity_name} \u95dc\u9589", + "is_on": "{entity_name} \u958b\u555f", + "is_open": "{entity_name} \u958b\u555f", + "is_plugged_in": "{entity_name} \u63d2\u5165", + "is_powered": "{entity_name} \u901a\u96fb", + "is_present": "{entity_name} \u51fa\u73fe", + "is_problem": "{entity_name} \u6b63\u5075\u6e2c\u5230\u554f\u984c", + "is_smoke": "{entity_name} \u6b63\u5075\u6e2c\u5230\u7159\u9727", + "is_sound": "{entity_name} \u6b63\u5075\u6e2c\u5230\u8072\u97f3", + "is_unsafe": "{entity_name} \u4e0d\u5b89\u5168", + "is_vibration": "{entity_name} \u6b63\u5075\u6e2c\u5230\u9707\u52d5" + }, + "trigger_type": { + "bat_low": "{entity_name} \u96fb\u91cf\u4f4e", + "closed": "{entity_name} \u5df2\u95dc\u9589", + "cold": "{entity_name} \u5df2\u8b8a\u51b7", + "connected": "{entity_name} \u5df2\u9023\u7dda", + "gas": "{entity_name} \u5df2\u958b\u59cb\u5075\u6e2c\u6c23\u9ad4", + "hot": "{entity_name} \u5df2\u8b8a\u71b1", + "light": "{entity_name} \u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda", + "locked": "{entity_name} \u5df2\u4e0a\u9396", + "moist\u00a7": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5", + "motion": "{entity_name} \u5df2\u5075\u6e2c\u5230\u52d5\u4f5c", + "moving": "{entity_name} \u958b\u59cb\u79fb\u52d5", + "no_gas": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u6c23\u9ad4", + "no_light": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u5149\u7dda", + "no_motion": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u52d5\u4f5c", + "no_problem": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u554f\u984c", + "no_smoke": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u7159\u9727", + "no_sound": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u8072\u97f3", + "no_vibration": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u9707\u52d5", + "not_bat_low": "{entity_name} \u96fb\u91cf\u6b63\u5e38", + "not_cold": "{entity_name} \u5df2\u4e0d\u51b7", + "not_connected": "{entity_name} \u5df2\u65b7\u7dda", + "not_hot": "{entity_name} \u5df2\u4e0d\u71b1", + "not_locked": "{entity_name} \u5df2\u89e3\u9396", + "not_moist": "{entity_name} \u5df2\u8b8a\u4e7e", + "not_moving": "{entity_name} \u505c\u6b62\u79fb\u52d5", + "not_occupied": "{entity_name} \u672a\u6709\u4eba", + "not_plugged_in": "{entity_name} \u672a\u63d2\u5165", + "not_powered": "{entity_name} \u672a\u901a\u96fb", + "not_present": "{entity_name} \u672a\u51fa\u73fe", + "not_unsafe": "{entity_name} \u5df2\u5b89\u5168", + "occupied": "{entity_name} \u8b8a\u6210\u6709\u4eba", + "opened": "{entity_name} \u5df2\u958b\u555f", + "plugged_in": "{entity_name} \u5df2\u63d2\u5165", + "powered": "{entity_name} \u5df2\u901a\u96fb", + "present": "{entity_name} \u5df2\u51fa\u73fe", + "problem": "{entity_name} \u5df2\u5075\u6e2c\u5230\u554f\u984c", + "smoke": "{entity_name} \u5df2\u5075\u6e2c\u5230\u7159\u9727", + "sound": "{entity_name} \u5df2\u5075\u6e2c\u5230\u8072\u97f3", + "turned_off": "{entity_name} \u5df2\u95dc\u9589", + "turned_on": "{entity_name} \u5df2\u958b\u555f", + "unsafe": "{entity_name} \u5df2\u4e0d\u5b89\u5168", + "vibration": "{entity_name} \u5df2\u5075\u6e2c\u5230\u9707\u52d5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/no.json b/homeassistant/components/cast/.translations/no.json index d36c929e7211b5..6b8166f23c0cd4 100644 --- a/homeassistant/components/cast/.translations/no.json +++ b/homeassistant/components/cast/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen Google Cast enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av Google Cast er n\u00f8dvendig." + "single_instance_allowed": "Kun en konfigurasjon av Google Cast er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/cast/.translations/zh-Hant.json b/homeassistant/components/cast/.translations/zh-Hant.json index d5383fb1a2bdba..711ac3203978c6 100644 --- a/homeassistant/components/cast/.translations/zh-Hant.json +++ b/homeassistant/components/cast/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u88dd\u7f6e\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u8a2d\u5099\u3002", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Google Cast \u5373\u53ef\u3002" }, "step": { diff --git a/homeassistant/components/daikin/.translations/zh-Hant.json b/homeassistant/components/daikin/.translations/zh-Hant.json index 1699bcad8f08bd..457b7d1b89c523 100644 --- a/homeassistant/components/daikin/.translations/zh-Hant.json +++ b/homeassistant/components/daikin/.translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "device_fail": "\u5275\u5efa\u88dd\u7f6e\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002", - "device_timeout": "\u9023\u7dda\u81f3\u88dd\u7f6e\u903e\u6642\u3002" + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "device_fail": "\u5275\u5efa\u8a2d\u5099\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002", + "device_timeout": "\u9023\u7dda\u81f3\u8a2d\u5099\u903e\u6642\u3002" }, "step": { "user": { diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json index 0f4bdf98ac14d5..42b14833aa0d54 100644 --- a/homeassistant/components/deconz/.translations/cs.json +++ b/homeassistant/components/deconz/.translations/cs.json @@ -23,7 +23,7 @@ "options": { "data": { "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel", - "allow_deconz_groups": "Povolit import skupin deCONZ " + "allow_deconz_groups": "Povolit import skupin deCONZ" }, "title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ" } diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 1bc6c8211a268d..cb5db0b8348ea4 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -59,13 +59,13 @@ }, "trigger_type": { "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces consecutivas", - "remote_button_long_press": "bot\u00f3n \"{subtype}\" pulsado continuamente", + "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" liberado despu\u00e9s de un rato pulsado", "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", - "remote_button_short_press": "bot\u00f3n \"{subtype}\" pulsado", - "remote_button_short_release": "bot\u00f3n \"{subtype}\" liberado", + "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", + "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_gyro_activated": "Dispositivo sacudido" } diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 3968c1f00c58d2..c7079fd62193e7 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -65,7 +65,7 @@ "remote_button_quintuple_press": "\" {subtype} \" - knappen femdobbelt klikket", "remote_button_rotated": "Knappen roterte \" {subtype} \"", "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", - "remote_button_short_release": "\" {subtype} \" -knappen ble utgitt", + "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\" {subtype} \"-knappen trippel klikket", "remote_gyro_activated": "Enhet er ristet" } diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index f024386aa0f873..bd47a637761ccd 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -4,9 +4,9 @@ "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "Bridge \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", - "not_deconz_bridge": "\u975e deCONZ Bridge \u88dd\u7f6e", + "not_deconz_bridge": "\u975e deCONZ Bridge \u8a2d\u5099", "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u7269\u4ef6", - "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u5be6\u4f8b" + "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u7269\u4ef6" }, "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" @@ -77,14 +77,14 @@ "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" }, - "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b" + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b" }, "deconz_devices": { "data": { "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" }, - "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b" + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b" } } } diff --git a/homeassistant/components/dialogflow/.translations/cs.json b/homeassistant/components/dialogflow/.translations/cs.json index 21da9b4823b6ec..db41ee98e01740 100644 --- a/homeassistant/components/dialogflow/.translations/cs.json +++ b/homeassistant/components/dialogflow/.translations/cs.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Povolena je pouze jedna instance." }, "create_entry": { - "default": "Chcete-li odeslat ud\u00e1losti do aplikace Home Assistant, budete muset nastavit [integraci Dialogflow]({dialogflow_url}). \n\n Vypl\u0148te n\u00e1sleduj\u00edc\u00ed informace: \n\n - URL: `{webhook_url}' \n - Metoda: POST \n - Typ obsahu: aplikace/json \n\n Podrobn\u011bj\u0161\u00ed informace naleznete v [dokumentaci]({docs_url})." + "default": "Chcete-li odeslat ud\u00e1losti do aplikace Home Assistant, budete muset nastavit [integraci Dialogflow]({dialogflow_url}). \n\n Vypl\u0148te n\u00e1sleduj\u00edc\u00ed informace: \n\n - URL: `{webhook_url}' \n - Metoda: POST \n - Typ obsahu: application/json\n\n Podrobn\u011bj\u0161\u00ed informace naleznete v [dokumentaci]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/fr.json b/homeassistant/components/dialogflow/.translations/fr.json index e9eabeff6381d9..0be75b94be94b9 100644 --- a/homeassistant/components/dialogflow/.translations/fr.json +++ b/homeassistant/components/dialogflow/.translations/fr.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Une seule instance est n\u00e9cessaire." }, "create_entry": { - "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer [Webhooks avec Mailgun] ( {dialogflow_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation] ( {docs_url} ) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer [Webhooks avec Dialogflow] ( {dialogflow_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation] ( {docs_url} ) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/no.json b/homeassistant/components/dialogflow/.translations/no.json index e27d59a40e3590..4d23ac8aaba79b 100644 --- a/homeassistant/components/dialogflow/.translations/no.json +++ b/homeassistant/components/dialogflow/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant forekomst m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta Dialogflow meldinger.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [webhook integrasjon av Dialogflow]({dialogflow_url}). \n\nFyll ut f\u00f8lgende informasjon: \n\n- URL: `{webhook_url}` \n- Metode: POST\n- Innholdstype: application/json\n\nSe [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/dialogflow/.translations/sl.json b/homeassistant/components/dialogflow/.translations/sl.json index 18a476b6870ebe..b6bd30f7997a2c 100644 --- a/homeassistant/components/dialogflow/.translations/sl.json +++ b/homeassistant/components/dialogflow/.translations/sl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Potrebna je samo ena instanca." }, "create_entry": { - "default": "Za po\u0161iljanje dogodkov Home Assistant-u, boste morali nastaviti [webhook z dialogflow]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) za nadaljna navodila." + "default": "Za po\u0161iljanje dogodkov Home Assistant-u, boste morali nastaviti [webhook z Dialogflow]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) za nadaljna navodila." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/zh-Hant.json b/homeassistant/components/dialogflow/.translations/zh-Hant.json index 18d3d92e16b55e..3cb54145ad857d 100644 --- a/homeassistant/components/dialogflow/.translations/zh-Hant.json +++ b/homeassistant/components/dialogflow/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Dialogflow \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Dialogflow \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/ecobee/.translations/da.json b/homeassistant/components/ecobee/.translations/da.json new file mode 100644 index 00000000000000..7a42a9470db52a --- /dev/null +++ b/homeassistant/components/ecobee/.translations/da.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "one_instance_only": "Integrationen underst\u00f8tter kun \u00e9n ecobee forekomst" + }, + "error": { + "pin_request_failed": "Fejl ved anmodning om pinkode fra ecobee. Kontroller at API-n\u00f8glen er korrekt.", + "token_request_failed": "Fejl ved anmodning om tokens fra ecobee. Pr\u00f8v igen." + }, + "step": { + "authorize": { + "title": "Autoriser app p\u00e5 ecobee.com" + }, + "user": { + "data": { + "api_key": "API-n\u00f8gle" + }, + "description": "Indtast API-n\u00f8glen, du har f\u00e5et fra ecobee.com.", + "title": "ecobee API-n\u00f8gle" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/no.json b/homeassistant/components/ecobee/.translations/no.json new file mode 100644 index 00000000000000..2bf141f6489288 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare en ecobee-forekomst." + }, + "error": { + "pin_request_failed": "Feil under foresp\u00f8rsel om PIN-kode fra ecobee. Kontroller at API-n\u00f8kkelen er riktig.", + "token_request_failed": "Feil ved foresp\u00f8rsel om tokener fra ecobee; Pr\u00f8v p\u00e5 nytt." + }, + "step": { + "authorize": { + "description": "Vennligst autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kode:\n\n{pin}\n\nDeretter, trykk p\u00e5 Send.", + "title": "Autoriser app p\u00e5 ecobee.com" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Vennligst skriv inn API-n\u00f8kkel som er innhentet fra ecobee.com.", + "title": "ecobee API-n\u00f8kkel" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/ru.json b/homeassistant/components/ecobee/.translations/ru.json new file mode 100644 index 00000000000000..660e0064bb6a74 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 ecobee." + }, + "error": { + "pin_request_failed": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 PIN-\u043a\u043e\u0434\u0430 \u0443 ecobee; \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043a\u043b\u044e\u0447\u0430 API.", + "token_request_failed": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0442\u043e\u043a\u0435\u043d\u043e\u0432 \u0443 ecobee; \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437." + }, + "step": { + "authorize": { + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 https://www.ecobee.com/consumerportal/index.html \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e PIN-\u043a\u043e\u0434\u0430: \n\n {pin} \n \n \u0417\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0430 ecobee.com" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 API, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043e\u0442 ecobee.com.", + "title": "ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/sv.json b/homeassistant/components/ecobee/.translations/sv.json new file mode 100644 index 00000000000000..f4a63bb449d1f8 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/zh-Hant.json b/homeassistant/components/ecobee/.translations/zh-Hant.json new file mode 100644 index 00000000000000..e1eb6ebd3570b1 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "\u6b64\u6574\u5408\u76ee\u524d\u50c5\u652f\u63f4\u4e00\u7d44 ecobee \u7269\u4ef6" + }, + "error": { + "pin_request_failed": "ecobee \u6240\u9700\u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d\u5bc6\u9470\u6b63\u78ba\u6027\u3002", + "token_request_failed": "ecobee \u6240\u9700\u5bc6\u9470\u932f\u8aa4\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" + }, + "step": { + "authorize": { + "description": "\u8acb\u65bc https://www.ecobee.com/consumerportal/index.html \u8f38\u5165\u4e0b\u65b9\u4ee3\u78bc\uff0c\u8a8d\u8b49\u6b64 App\uff1a\n\n{pin}\n\n\u7136\u5f8c\u6309\u4e0b\u300cSubmit\u300d\u3002", + "title": "\u65bc ecobee.com \u4e0a\u8a8d\u8b49 App" + }, + "user": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "description": "\u8acb\u8f38\u5165\u7531 ecobee.com \u6240\u7372\u5f97\u7684 API \u5bc6\u9470\u3002", + "title": "ecobee API \u5bc6\u9470" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/es.json b/homeassistant/components/esphome/.translations/es.json index 70d766cf4c0544..be8033f316a2c9 100644 --- a/homeassistant/components/esphome/.translations/es.json +++ b/homeassistant/components/esphome/.translations/es.json @@ -8,7 +8,7 @@ "invalid_password": "\u00a1Contrase\u00f1a incorrecta!", "resolve_error": "No se puede resolver la direcci\u00f3n de ESP. Si el error persiste, configura una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "Desplom\u00e9: {name}", + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/geofency/.translations/no.json b/homeassistant/components/geofency/.translations/no.json index 4409616cef497d..1956c453a9f084 100644 --- a/homeassistant/components/geofency/.translations/no.json +++ b/homeassistant/components/geofency/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Geofency.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 kunne sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Geofency. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/geofency/.translations/zh-Hant.json b/homeassistant/components/geofency/.translations/zh-Hant.json index bec33c26d100b6..003a3db8bf181b 100644 --- a/homeassistant/components/geofency/.translations/zh-Hant.json +++ b/homeassistant/components/geofency/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Geofency \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Geofency \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/gpslogger/.translations/no.json b/homeassistant/components/gpslogger/.translations/no.json index 836b5c8bc687eb..488a09c3768693 100644 --- a/homeassistant/components/gpslogger/.translations/no.json +++ b/homeassistant/components/gpslogger/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra GPSLogger.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i GPSLogger. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/gpslogger/.translations/zh-Hant.json b/homeassistant/components/gpslogger/.translations/zh-Hant.json index c9d98da1afcf0e..c21e76a6eee209 100644 --- a/homeassistant/components/gpslogger/.translations/zh-Hant.json +++ b/homeassistant/components/gpslogger/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 GPSLogger \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 GPSLogger \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/heos/.translations/no.json b/homeassistant/components/heos/.translations/no.json index dd4cb48a0906c1..d41051b6674eb8 100644 --- a/homeassistant/components/heos/.translations/no.json +++ b/homeassistant/components/heos/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Du kan kun konfigurere en enkelt Heos tilkobling, da den st\u00f8tter alle enhetene p\u00e5 nettverket." + "already_setup": "Du kan kun konfigurere en Heos tilkobling, da den st\u00f8tter alle enhetene p\u00e5 nettverket." }, "error": { "connection_failure": "Kan ikke koble til den angitte verten." diff --git a/homeassistant/components/heos/.translations/zh-Hant.json b/homeassistant/components/heos/.translations/zh-Hant.json index c45f9c467e4a8e..9efacaf163f9d2 100644 --- a/homeassistant/components/heos/.translations/zh-Hant.json +++ b/homeassistant/components/heos/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Heos \u9023\u7dda\uff0c\u5c07\u652f\u63f4\u7db2\u8def\u4e2d\u6240\u6709\u5c0d\u61c9\u88dd\u7f6e\u3002" + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Heos \u9023\u7dda\uff0c\u5c07\u652f\u63f4\u7db2\u8def\u4e2d\u6240\u6709\u5c0d\u61c9\u8a2d\u5099\u3002" }, "error": { "connection_failure": "\u7121\u6cd5\u9023\u7dda\u81f3\u6307\u5b9a\u4e3b\u6a5f\u7aef\u3002" @@ -12,7 +12,7 @@ "access_token": "\u4e3b\u6a5f\u7aef", "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u6bb5\u540d\u7a31\u6216 Heos \u88dd\u7f6e IP \u4f4d\u5740\uff08\u5df2\u900f\u904e\u6709\u7dda\u7db2\u8def\u9023\u7dda\uff09\u3002", + "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u6bb5\u540d\u7a31\u6216 Heos \u8a2d\u5099 IP \u4f4d\u5740\uff08\u5df2\u900f\u904e\u6709\u7dda\u7db2\u8def\u9023\u7dda\uff09\u3002", "title": "\u9023\u7dda\u81f3 Heos" } }, diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hant.json b/homeassistant/components/homekit_controller/.translations/zh-Hant.json index 7340569e64ff74..68e87e9aea8bee 100644 --- a/homeassistant/components/homekit_controller/.translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/.translations/zh-Hant.json @@ -1,20 +1,20 @@ { "config": { "abort": { - "accessory_not_found_error": "\u627e\u4e0d\u5230\u88dd\u7f6e\uff0c\u7121\u6cd5\u65b0\u589e\u914d\u5c0d\u3002", + "accessory_not_found_error": "\u627e\u4e0d\u5230\u8a2d\u5099\uff0c\u7121\u6cd5\u65b0\u589e\u914d\u5c0d\u3002", "already_configured": "\u914d\u4ef6\u5df2\u7d93\u7531\u6b64\u63a7\u5236\u5668\u8a2d\u5b9a\u5b8c\u6210", - "already_in_progress": "\u88dd\u7f6e\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", - "already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u88dd\u7f6e\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", + "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u8a2d\u5099\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", "ignored_model": "\u7531\u65bc\u6b64\u578b\u865f\u53ef\u539f\u751f\u652f\u63f4\u66f4\u5b8c\u6574\u529f\u80fd\uff0c\u56e0\u6b64 Homekit \u652f\u63f4\u5df2\u88ab\u7981\u6b62\u3002", - "invalid_config_entry": "\u88dd\u7f6e\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", - "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u88dd\u7f6e" + "invalid_config_entry": "\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", + "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u8a2d\u5099" }, "error": { "authentication_error": "Homekit \u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u5b9a\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", - "busy_error": "\u88dd\u7f6e\u5df2\u7d93\u8207\u5176\u4ed6\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", - "max_peers_error": "\u88dd\u7f6e\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", - "max_tries_error": "\u88dd\u7f6e\u6536\u5230\u8d85\u904e 100 \u6b21\u672a\u6210\u529f\u8a8d\u8b49\u5f8c\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", - "pairing_failed": "\u7576\u8a66\u5716\u8207\u88dd\u7f6e\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u88dd\u7f6e\u76ee\u524d\u4e0d\u652f\u63f4\u3002", + "busy_error": "\u8a2d\u5099\u5df2\u7d93\u8207\u5176\u4ed6\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", + "max_peers_error": "\u8a2d\u5099\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", + "max_tries_error": "\u8a2d\u5099\u6536\u5230\u8d85\u904e 100 \u6b21\u672a\u6210\u529f\u8a8d\u8b49\u5f8c\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", + "pairing_failed": "\u7576\u8a66\u5716\u8207\u8a2d\u5099\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u8a2d\u5099\u76ee\u524d\u4e0d\u652f\u63f4\u3002", "unable_to_pair": "\u7121\u6cd5\u914d\u5c0d\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "unknown_error": "\u88dd\u7f6e\u56de\u5831\u672a\u77e5\u932f\u8aa4\u3002\u914d\u5c0d\u5931\u6557\u3002" }, diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json index f95ccf572723d9..c6a960f1a68367 100644 --- a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json +++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json @@ -15,7 +15,7 @@ "init": { "data": { "hapid": "Access point ID (SGTIN)", - "name": "\u540d\u7a31\uff08\u9078\u9805\uff0c\u7528\u4ee5\u4f5c\u70ba\u6240\u6709\u88dd\u7f6e\u7684\u5b57\u9996\u7528\uff09", + "name": "\u540d\u7a31\uff08\u9078\u9805\uff0c\u7528\u4ee5\u4f5c\u70ba\u6240\u6709\u8a2d\u5099\u7684\u5b57\u9996\u7528\uff09", "pin": "PIN \u78bc\uff08\u9078\u9805\uff09" }, "title": "\u9078\u64c7 HomematicIP Access point" diff --git a/homeassistant/components/hue/.translations/zh-Hant.json b/homeassistant/components/hue/.translations/zh-Hant.json index 7a08b44f25afa7..6bbe75a8019b33 100644 --- a/homeassistant/components/hue/.translations/zh-Hant.json +++ b/homeassistant/components/hue/.translations/zh-Hant.json @@ -7,7 +7,7 @@ "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Bridge", "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", "no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge", - "not_hue_bridge": "\u975e Hue Bridge \u88dd\u7f6e", + "not_hue_bridge": "\u975e Hue Bridge \u8a2d\u5099", "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/ifttt/.translations/no.json b/homeassistant/components/ifttt/.translations/no.json index 481ab372e91d0a..2fe38659fadb93 100644 --- a/homeassistant/components/ifttt/.translations/no.json +++ b/homeassistant/components/ifttt/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant enhet m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta IFTTT-meldinger.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du bruke \"Make a web request\" handlingen fra [IFTTT Webhook applet]({applet_url}).\n\nFyll ut f\u00f8lgende informasjon:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSe [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." diff --git a/homeassistant/components/ios/.translations/no.json b/homeassistant/components/ios/.translations/no.json index a125b96a070ac5..798ae93d33b2dd 100644 --- a/homeassistant/components/ios/.translations/no.json +++ b/homeassistant/components/ios/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Kun en enkelt konfigurasjon av Home Assistant iOS er n\u00f8dvendig." + "single_instance_allowed": "Kun en konfigurasjon av Home Assistant iOS er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/izone/.translations/no.json b/homeassistant/components/izone/.translations/no.json index fcd5c1df019a6b..9068b18c82df33 100644 --- a/homeassistant/components/izone/.translations/no.json +++ b/homeassistant/components/izone/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Finner ingen iZone-enheter p\u00e5 nettverket.", - "single_instance_allowed": "Bare en enkelt konfigurasjon av iZone er n\u00f8dvendig." + "single_instance_allowed": "Bare en konfigurasjon av iZone er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/.translations/no.json b/homeassistant/components/lifx/.translations/no.json index 63080a30ff16a3..ae32d43f1b6aaf 100644 --- a/homeassistant/components/lifx/.translations/no.json +++ b/homeassistant/components/lifx/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen LIFX-enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av LIFX er mulig." + "single_instance_allowed": "Kun en konfigurasjon av LIFX er mulig." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/.translations/zh-Hant.json b/homeassistant/components/lifx/.translations/zh-Hant.json index 4c66f0d01333d3..908394bcfd81c0 100644 --- a/homeassistant/components/lifx/.translations/zh-Hant.json +++ b/homeassistant/components/lifx/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 LIFX \u88dd\u7f6e\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 LIFX \u8a2d\u5099\u3002", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 LIFX\u3002" }, "step": { diff --git a/homeassistant/components/light/.translations/ca.json b/homeassistant/components/light/.translations/ca.json index c9b727088ab180..b8b3bbb8125e8b 100644 --- a/homeassistant/components/light/.translations/ca.json +++ b/homeassistant/components/light/.translations/ca.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "Commuta {name}", - "turn_off": "Apaga {name}", - "turn_on": "Enc\u00e9n {name}" + "toggle": "Commuta {entity_name}", + "turn_off": "Apaga {entity_name}", + "turn_on": "Enc\u00e9n {entity_name}" }, "condition_type": { - "is_off": "{name} est\u00e0 apagat", - "is_on": "{name} est\u00e0 enc\u00e8s" + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s" }, "trigger_type": { - "turned_off": "{name} apagat", - "turned_on": "{name} enc\u00e8s" + "turned_off": "{entity_name} apagat", + "turned_on": "{entity_name} enc\u00e8s" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json index 4ea4a94014ea83..14a747f6effeb8 100644 --- a/homeassistant/components/light/.translations/da.json +++ b/homeassistant/components/light/.translations/da.json @@ -1,8 +1,8 @@ { "device_automation": { "trigger_type": { - "turned_off": "{name} slukket", - "turned_on": "{name} t\u00e6ndt" + "turned_off": "{entity_name} slukket", + "turned_on": "{entity_name} t\u00e6ndt" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json index 2fe1c6b42dcd42..e07adeb0a36e84 100644 --- a/homeassistant/components/light/.translations/de.json +++ b/homeassistant/components/light/.translations/de.json @@ -1,8 +1,8 @@ { "device_automation": { "trigger_type": { - "turned_off": "{name} ausgeschaltet", - "turned_on": "{name} eingeschaltet" + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/hu.json b/homeassistant/components/light/.translations/hu.json new file mode 100644 index 00000000000000..7d7e158f3cb064 --- /dev/null +++ b/homeassistant/components/light/.translations/hu.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} fel/lekapcsol\u00e1sa", + "turn_off": "{entity_name} lekapcsol\u00e1sa", + "turn_on": "{entity_name} felkapcsol\u00e1sa" + }, + "condition_type": { + "is_off": "{entity_name} le van kapcsolva", + "is_on": "{entity_name} fel van kapcsolva" + }, + "trigger_type": { + "turned_off": "{entity_name} le lett kapcsolva", + "turned_on": "{entity_name} fel lett kapcsolva" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/nl.json b/homeassistant/components/light/.translations/nl.json index 63954ca83a9fa1..e742a337cca0fa 100644 --- a/homeassistant/components/light/.translations/nl.json +++ b/homeassistant/components/light/.translations/nl.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "Omschakelen {naam}", + "toggle": "Omschakelen {entity_name}", "turn_off": "{entity_name} uitschakelen", "turn_on": "{entity_name} inschakelen" }, "condition_type": { - "is_off": "{name} is uitgeschakeld", - "is_on": "{name} is ingeschakeld" + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld" }, "trigger_type": { - "turned_off": "{name} is uitgeschakeld", - "turned_on": "{name} is ingeschakeld" + "turned_off": "{entity_name} is uitgeschakeld", + "turned_on": "{entity_name} is ingeschakeld" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index f8f4a2761d4624..17c81c471f5018 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -7,11 +7,11 @@ }, "condition_type": { "is_off": "{entity_name} jest wy\u0142\u0105czone", - "is_on": "(entity_name} jest w\u0142\u0105czony." + "is_on": "{entity_name} jest w\u0142\u0105czony." }, "trigger_type": { - "turned_off": "{nazwa} wy\u0142\u0105czone", - "turned_on": "{name} w\u0142\u0105czone" + "turned_off": "{entity_name} wy\u0142\u0105czone", + "turned_on": "{entity_name} w\u0142\u0105czone" } } } \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/it.json b/homeassistant/components/locative/.translations/it.json index de62d2ac2f772b..4fdef0a987e0ab 100644 --- a/homeassistant/components/locative/.translations/it.json +++ b/homeassistant/components/locative/.translations/it.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { - "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 Webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/locative/.translations/no.json b/homeassistant/components/locative/.translations/no.json index 8e9b3272f947b3..c5ad304300479f 100644 --- a/homeassistant/components/locative/.translations/no.json +++ b/homeassistant/components/locative/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Geofency.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 kunne sende steder til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Locative. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/locative/.translations/zh-Hant.json b/homeassistant/components/locative/.translations/zh-Hant.json index 62bb6bb9d962a4..7dd598c8fc217d 100644 --- a/homeassistant/components/locative/.translations/zh-Hant.json +++ b/homeassistant/components/locative/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Locative \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Locative \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/logi_circle/.translations/no.json b/homeassistant/components/logi_circle/.translations/no.json index c68f49509baae8..23b951bfa621bf 100644 --- a/homeassistant/components/logi_circle/.translations/no.json +++ b/homeassistant/components/logi_circle/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Du kan bare konfigurere en enkelt Logi Circle konto.", + "already_setup": "Du kan bare konfigurere en Logi Circle konto.", "external_error": "Det oppstod et unntak fra en annen flow.", "external_setup": "Logi Circle er vellykket konfigurert fra en annen flow.", "no_flows": "Du m\u00e5 konfigurere Logi Circle f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/logi_circle/)." diff --git a/homeassistant/components/logi_circle/.translations/zh-Hant.json b/homeassistant/components/logi_circle/.translations/zh-Hant.json index b9f82b6e2e507a..1eb3b71c942d77 100644 --- a/homeassistant/components/logi_circle/.translations/zh-Hant.json +++ b/homeassistant/components/logi_circle/.translations/zh-Hant.json @@ -7,7 +7,7 @@ "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Logi Circle \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/logi_circle/\uff09\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Logi Circle \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Logi Circle \u8a2d\u5099\u3002" }, "error": { "auth_error": "API \u8a8d\u8b49\u5931\u6557\u3002", diff --git a/homeassistant/components/mailgun/.translations/no.json b/homeassistant/components/mailgun/.translations/no.json index 91c616b69af789..3d1cbd41a34172 100644 --- a/homeassistant/components/mailgun/.translations/no.json +++ b/homeassistant/components/mailgun/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant forekomst m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 motta Mailgun-meldinger.", - "one_instance_allowed": "Kun \u00e9n enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [Webhooks with Mailgun]({mailgun_url}).\n\nFyll ut f\u00f8lgende informasjon:\n\n- URL: `{webhook_url}`\n- Metode: POST\n- Innholdstype: application/json\n\nSe [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." diff --git a/homeassistant/components/mailgun/.translations/zh-Hant.json b/homeassistant/components/mailgun/.translations/zh-Hant.json index 4b9ab3a7abb782..f16533312fa21a 100644 --- a/homeassistant/components/mailgun/.translations/zh-Hant.json +++ b/homeassistant/components/mailgun/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Mailgun \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Mailgun \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json index b3f1e4740b9e67..99a760dce4a273 100644 --- a/homeassistant/components/mqtt/.translations/no.json +++ b/homeassistant/components/mqtt/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Kun en enkelt konfigurasjon av MQTT er tillatt." + "single_instance_allowed": "Kun en konfigurasjon av MQTT er tillatt." }, "error": { "cannot_connect": "Kan ikke koble til megleren." diff --git a/homeassistant/components/nest/.translations/no.json b/homeassistant/components/nest/.translations/no.json index 03cf1a82b813bf..743c47e00c8f46 100644 --- a/homeassistant/components/nest/.translations/no.json +++ b/homeassistant/components/nest/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Du kan bare konfigurere en enkelt Nest konto.", + "already_setup": "Du kan bare konfigurere en Nest konto.", "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", "no_flows": "Du m\u00e5 konfigurere Nest f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/nest/)." diff --git a/homeassistant/components/notion/.translations/zh-Hant.json b/homeassistant/components/notion/.translations/zh-Hant.json index af89dd3d39b4a5..f672f519f408de 100644 --- a/homeassistant/components/notion/.translations/zh-Hant.json +++ b/homeassistant/components/notion/.translations/zh-Hant.json @@ -3,7 +3,7 @@ "error": { "identifier_exists": "\u4f7f\u7528\u8005\u540d\u7a31\u5df2\u8a3b\u518a", "invalid_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u7121\u6548", - "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u8a2d\u5099" }, "step": { "user": { diff --git a/homeassistant/components/owntracks/.translations/no.json b/homeassistant/components/owntracks/.translations/no.json index 9f86cd12cc4f52..5838dcad30bc28 100644 --- a/homeassistant/components/owntracks/.translations/no.json +++ b/homeassistant/components/owntracks/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "one_instance_allowed": "Kun \u00e9n enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "\n\nP\u00e5 Android, \u00e5pne [OwnTracks appen]({android_url}), g\u00e5 til Instillinger -> tilkobling. Endre f\u00f8lgende innstillinger: \n - Modus: Privat HTTP\n - Vert: {webhook_url} \n - Identifikasjon: \n - Brukernavn: ` ` \n - Enhets-ID: ` ` \n\nP\u00e5 iOS, \u00e5pne [OwnTracks appen]({ios_url}), trykk p\u00e5 (i) ikonet \u00f8verst til venstre - > innstillinger. Endre f\u00f8lgende innstillinger: \n - Modus: HTTP \n - URL: {webhook_url} \n - Sl\u00e5 p\u00e5 autentisering \n - BrukerID: ` ` \n\n {secret} \n \n Se [dokumentasjonen]({docs_url}) for mer informasjon." diff --git a/homeassistant/components/owntracks/.translations/zh-Hant.json b/homeassistant/components/owntracks/.translations/zh-Hant.json index d8c195cb27738f..923f452450b035 100644 --- a/homeassistant/components/owntracks/.translations/zh-Hant.json +++ b/homeassistant/components/owntracks/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { - "default": "\n\n\u65bc Android \u88dd\u7f6e\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a ``\n - Device ID\uff1a``\n\n\u65bc iOS \u88dd\u7f6e\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: ``\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + "default": "\n\n\u65bc Android \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a ``\n - Device ID\uff1a``\n\n\u65bc iOS \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: ``\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/no.json b/homeassistant/components/plaato/.translations/no.json index 4b47f52eef915c..0de31a35eb2937 100644 --- a/homeassistant/components/plaato/.translations/no.json +++ b/homeassistant/components/plaato/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Plaato Airlock.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Plaato Airlock. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/plaato/.translations/zh-Hant.json b/homeassistant/components/plaato/.translations/zh-Hant.json index 20cdb405f4ee10..1bf211476d8895 100644 --- a/homeassistant/components/plaato/.translations/zh-Hant.json +++ b/homeassistant/components/plaato/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Plaato Airlock \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Plaato Airlock \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index ea680a638eb471..670bc23ca1fd3b 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Vis alle kontrolelementer", + "use_episode_art": "Brug episode kunst" + }, + "description": "Indstillinger for Plex-medieafspillere" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index 7fa9f62be07763..d3c4c0d5a7825b 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Show all controls", + "use_episode_art": "Use episode art" + }, + "description": "Options for Plex Media Players" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index b58cdfe728e043..393639dd4c9fc9 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -41,5 +41,15 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Vis alle kontroller" + }, + "description": "Alternativer for Plex Media Players" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index b424be059f9a5c..c547e4306b4ad5 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f", + "use_episode_art": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u043b\u043e\u0436\u043a\u0438 \u044d\u043f\u0438\u0437\u043e\u0434\u043e\u0432" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/sv.json b/homeassistant/components/plex/.translations/sv.json new file mode 100644 index 00000000000000..702cec128c0353 --- /dev/null +++ b/homeassistant/components/plex/.translations/sv.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Visa alla kontroller" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index c79a49470e000d..2cf3fa2c1a7e81 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "\u9a57\u8b49\u5931\u6557", "no_servers": "\u6b64\u5e33\u865f\u672a\u7d81\u5b9a\u4f3a\u670d\u5668", + "no_token": "\u63d0\u4f9b\u5bc6\u9470\u6216\u9078\u64c7\u624b\u52d5\u8a2d\u5b9a", "not_found": "\u627e\u4e0d\u5230 Plex \u4f3a\u670d\u5668" }, "step": { + "manual_setup": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u4f7f\u7528 SSL", + "token": "\u5bc6\u9470\uff08\u5982\u679c\u9700\u8981\uff09", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "title": "Plex \u4f3a\u670d\u5668" + }, "select_server": { "data": { "server": "\u4f3a\u670d\u5668" @@ -22,9 +33,10 @@ }, "user": { "data": { + "manual_setup": "\u624b\u52d5\u8a2d\u5b9a", "token": "Plex \u5bc6\u9470" }, - "description": "\u8acb\u8f38\u5165 Plex \u5bc6\u9470\u4ee5\u9032\u884c\u81ea\u52d5\u8a2d\u5b9a\u3002", + "description": "\u8acb\u8f38\u5165 Plex \u5bc6\u9470\u4ee5\u9032\u884c\u81ea\u52d5\u6216\u624b\u52d5\u8a2d\u5b9a\u4f3a\u670d\u5668\u3002", "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" } }, diff --git a/homeassistant/components/point/.translations/zh-Hant.json b/homeassistant/components/point/.translations/zh-Hant.json index 9f688b2e5f9d52..f1bbb1c872c3d7 100644 --- a/homeassistant/components/point/.translations/zh-Hant.json +++ b/homeassistant/components/point/.translations/zh-Hant.json @@ -8,7 +8,7 @@ "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Point \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15](https://www.home-assistant.io/components/point/)\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Minut Point \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Minut Point \u8a2d\u5099\u3002" }, "error": { "follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002", diff --git a/homeassistant/components/ps4/.translations/de.json b/homeassistant/components/ps4/.translations/de.json index 6d152108117226..2053d2f4a80bed 100644 --- a/homeassistant/components/ps4/.translations/de.json +++ b/homeassistant/components/ps4/.translations/de.json @@ -5,7 +5,7 @@ "devices_configured": "Alle gefundenen Ger\u00e4te sind bereits konfiguriert.", "no_devices_found": "Es wurden keine PlayStation 4 im Netzwerk gefunden.", "port_987_bind_error": "Bind to Port 987 nicht m\u00f6glich.", - "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen finden Sie in der [documentation](https://www.home-assistant.io/components/ps4/)" + "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen finden Sie in der [Dokumentation](https://www.home-assistant.io/components/ps4/)" }, "error": { "credential_timeout": "Zeit\u00fcberschreitung beim Warten auf den Anmeldedienst. Klicken zum Neustarten auf Senden.", diff --git a/homeassistant/components/ps4/.translations/zh-Hant.json b/homeassistant/components/ps4/.translations/zh-Hant.json index f0a71b4be5b533..a786b0c74d3f15 100644 --- a/homeassistant/components/ps4/.translations/zh-Hant.json +++ b/homeassistant/components/ps4/.translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "credential_error": "\u53d6\u5f97\u6191\u8b49\u932f\u8aa4\u3002", - "devices_configured": "\u6240\u6709\u88dd\u7f6e\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", + "devices_configured": "\u6240\u6709\u8a2d\u5099\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 PlayStation 4 \u88dd\u7f6e\u3002", "port_987_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 987\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", "port_997_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 997\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002" @@ -15,7 +15,7 @@ }, "step": { "creds": { - "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u88dd\u7f6e\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", + "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u8a2d\u5099\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", "title": "PlayStation 4" }, "link": { @@ -25,7 +25,7 @@ "name": "\u540d\u7a31", "region": "\u5340\u57df" }, - "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u88dd\u7f6e\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", + "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u8a2d\u5099\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", "title": "PlayStation 4" }, "mode": { @@ -33,7 +33,7 @@ "ip_address": "IP \u4f4d\u5740\uff08\u5982\u679c\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22\u65b9\u5f0f\uff0c\u8acb\u4fdd\u7559\u7a7a\u767d\uff09\u3002", "mode": "\u8a2d\u5b9a\u6a21\u5f0f" }, - "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u63a2\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u88dd\u7f6e\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", + "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u63a2\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u8a2d\u5099\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", "title": "PlayStation 4" } }, diff --git a/homeassistant/components/smartthings/.translations/he.json b/homeassistant/components/smartthings/.translations/he.json index c38afd989d250e..7f80900d8282e0 100644 --- a/homeassistant/components/smartthings/.translations/he.json +++ b/homeassistant/components/smartthings/.translations/he.json @@ -16,7 +16,7 @@ "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" }, "description": "\u05d4\u05d6\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea] ( {token_url} ) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea] ( {component_url} ).", - "title": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9 " + "title": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9 " }, "wait_install": { "description": "\u05d4\u05ea\u05e7\u05df \u05d0\u05ea \u05d4- Home Assistant SmartApp \u05dc\u05e4\u05d7\u05d5\u05ea \u05d1\u05de\u05d9\u05e7\u05d5\u05dd \u05d0\u05d7\u05d3 \u05d5\u05dc\u05d7\u05e5 \u05e2\u05dc \u05e9\u05dc\u05d7.", diff --git a/homeassistant/components/somfy/.translations/zh-Hant.json b/homeassistant/components/somfy/.translations/zh-Hant.json index 6b53e943304383..f7f7f4a1d8e33b 100644 --- a/homeassistant/components/somfy/.translations/zh-Hant.json +++ b/homeassistant/components/somfy/.translations/zh-Hant.json @@ -6,7 +6,7 @@ "missing_configuration": "Somfy \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Somfy \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Somfy \u8a2d\u5099\u3002" }, "title": "Somfy" } diff --git a/homeassistant/components/sonos/.translations/no.json b/homeassistant/components/sonos/.translations/no.json index c837abad499db4..ae47916ac85a2d 100644 --- a/homeassistant/components/sonos/.translations/no.json +++ b/homeassistant/components/sonos/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av Sonos er n\u00f8dvendig." + "single_instance_allowed": "Kun en konfigurasjon av Sonos er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/sonos/.translations/zh-Hant.json b/homeassistant/components/sonos/.translations/zh-Hant.json index 520a29b7602526..c6fb13c3605d30 100644 --- a/homeassistant/components/sonos/.translations/zh-Hant.json +++ b/homeassistant/components/sonos/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u88dd\u7f6e\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u8a2d\u5099\u3002", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Sonos \u5373\u53ef\u3002" }, "step": { diff --git a/homeassistant/components/tplink/.translations/no.json b/homeassistant/components/tplink/.translations/no.json index 4946eb81f02952..41148475a5f6f4 100644 --- a/homeassistant/components/tplink/.translations/no.json +++ b/homeassistant/components/tplink/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen TP-Link enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av TP-Link er n\u00f8dvendig." + "single_instance_allowed": "Kun en konfigurasjon av TP-Link er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/tplink/.translations/zh-Hant.json b/homeassistant/components/tplink/.translations/zh-Hant.json index d44faf195e55ea..250a5509c4c265 100644 --- a/homeassistant/components/tplink/.translations/zh-Hant.json +++ b/homeassistant/components/tplink/.translations/zh-Hant.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u88dd\u7f6e\uff1f", + "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u8a2d\u5099\uff1f", "title": "TP-Link Smart Home" } }, diff --git a/homeassistant/components/traccar/.translations/de.json b/homeassistant/components/traccar/.translations/de.json index c835ddf76b2481..dccd39b6997ff5 100644 --- a/homeassistant/components/traccar/.translations/de.json +++ b/homeassistant/components/traccar/.translations/de.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Es ist nur eine einzelne Instanz erforderlich." }, "create_entry": { - "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}}`\n\nSiehe [die Dokumentation]({docs_url}) f\u00fcr weitere Details." + "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}}`\n\nSiehe [die Dokumentation]( {docs_url} ) f\u00fcr weitere Details." }, "step": { "user": { diff --git a/homeassistant/components/traccar/.translations/no.json b/homeassistant/components/traccar/.translations/no.json index dea146b649aace..805b952690e577 100644 --- a/homeassistant/components/traccar/.translations/no.json +++ b/homeassistant/components/traccar/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant-forekomst m\u00e5 v\u00e6re tilgjengelig fra Internett for \u00e5 motta meldinger fra Traccar.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "Hvis du vil sende hendelser til Home Assistant, m\u00e5 du konfigurere webhook-funksjonen i Traccar.\n\nBruk f\u00f8lgende URL-adresse: ' {webhook_url} '\n\nSe [dokumentasjonen] ({docs_url}) for mer informasjon." diff --git a/homeassistant/components/traccar/.translations/zh-Hant.json b/homeassistant/components/traccar/.translations/zh-Hant.json index f5402454294856..85e8994dc55c34 100644 --- a/homeassistant/components/traccar/.translations/zh-Hant.json +++ b/homeassistant/components/traccar/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Traccar \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Traccar \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/transmission/.translations/da.json b/homeassistant/components/transmission/.translations/da.json new file mode 100644 index 00000000000000..3a619d8154af21 --- /dev/null +++ b/homeassistant/components/transmission/.translations/da.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", + "wrong_credentials": "Ugyldigt brugernavn eller adgangskode" + }, + "step": { + "options": { + "data": { + "scan_interval": "Opdateringsfrekvens" + }, + "title": "Konfigurer indstillinger" + }, + "user": { + "data": { + "host": "V\u00e6rt", + "name": "Navn", + "password": "Adgangskode", + "port": "Port", + "username": "Brugernavn" + }, + "title": "Konfigurer Transmission klient" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Opdateringsfrekvens" + }, + "description": "Konfigurationsindstillinger for Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index 7160cd109c4794..e1bc8dc322824a 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -1,39 +1,39 @@ { "config": { - "title": "Transmission", + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + }, + "error": { + "cannot_connect": "Unable to Connect to host", + "wrong_credentials": "Wrong username or password" + }, "step": { + "options": { + "data": { + "scan_interval": "Update frequency" + }, + "title": "Configure Options" + }, "user": { - "title": "Setup Transmission Client", "data": { - "name": "Name", "host": "Host", - "username": "User name", + "name": "Name", "password": "Password", - "port": "Port" - } - }, - "options": { - "title": "Configure Options", - "data": { - "scan_interval": "Update frequency" - } + "port": "Port", + "username": "User name" + }, + "title": "Setup Transmission Client" } }, - "error": { - "wrong_credentials": "Wrong username or password", - "cannot_connect": "Unable to Connect to host" - }, - "abort": { - "one_instance_allowed": "Only a single instance is necessary." - } + "title": "Transmission" }, "options": { "step": { "init": { - "description": "Configure options for Transmission", "data": { "scan_interval": "Update frequency" - } + }, + "description": "Configure options for Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/no.json b/homeassistant/components/transmission/.translations/no.json new file mode 100644 index 00000000000000..f6ddce2a4a799e --- /dev/null +++ b/homeassistant/components/transmission/.translations/no.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Bare en enkel instans er n\u00f8dvendig." + }, + "error": { + "cannot_connect": "Kan ikke koble til vert", + "wrong_credentials": "Ugyldig brukernavn eller passord" + }, + "step": { + "options": { + "data": { + "scan_interval": "Oppdater frekvens" + }, + "title": "Konfigurer alternativer" + }, + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + }, + "title": "Oppsett av klient for Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Oppdater frekvens" + }, + "description": "Konfigurer alternativer for Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json new file mode 100644 index 00000000000000..5da2d4f9ef909b --- /dev/null +++ b/homeassistant/components/transmission/.translations/ru.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443", + "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "step": { + "options": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/sv.json b/homeassistant/components/transmission/.translations/sv.json new file mode 100644 index 00000000000000..30004af17dbf98 --- /dev/null +++ b/homeassistant/components/transmission/.translations/sv.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "error": { + "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden", + "wrong_credentials": "Fel anv\u00e4ndarnamn eller l\u00f6senord" + }, + "step": { + "options": { + "data": { + "scan_interval": "Uppdateringsfrekvens" + }, + "title": "Konfigurera alternativ" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsfrekvens" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/no.json b/homeassistant/components/twilio/.translations/no.json index 0d28b094340f8b..c3d6ff16e2dd32 100644 --- a/homeassistant/components/twilio/.translations/no.json +++ b/homeassistant/components/twilio/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant forekomst m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 motta Twilio-meldinger.", - "one_instance_allowed": "Kun \u00e9n enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [Webhooks with Twilio]({twilio_url}). \n\nFyll ut f\u00f8lgende informasjon: \n\n- URL: `{webhook_url}` \n- Metode: POST\n- Innholdstype: application/x-www-form-urlencoded \n\n Se [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." diff --git a/homeassistant/components/twilio/.translations/zh-Hant.json b/homeassistant/components/twilio/.translations/zh-Hant.json index 2e85ef7b2ded10..858970539cd223 100644 --- a/homeassistant/components/twilio/.translations/zh-Hant.json +++ b/homeassistant/components/twilio/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Twilio \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Twilio \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json index 2d5bd9027ac44d..498afcbb10a19f 100644 --- a/homeassistant/components/unifi/.translations/zh-Hant.json +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -29,7 +29,7 @@ "data": { "detection_time": "\u6700\u7d42\u51fa\u73fe\u5f8c\u8996\u70ba\u96e2\u958b\u7684\u6642\u9593\uff08\u4ee5\u79d2\u70ba\u55ae\u4f4d\uff09", "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", - "track_devices": "\u8ffd\u8e64\u7db2\u8def\u88dd\u7f6e\uff08Ubiquiti \u88dd\u7f6e\uff09", + "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09", "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" } } diff --git a/homeassistant/components/upnp/.translations/no.json b/homeassistant/components/upnp/.translations/no.json index 813509121e34d5..fb1508a1aab031 100644 --- a/homeassistant/components/upnp/.translations/no.json +++ b/homeassistant/components/upnp/.translations/no.json @@ -6,7 +6,7 @@ "no_devices_discovered": "Ingen UPnP / IGDs oppdaget", "no_devices_found": "Ingen UPnP / IGD-enheter funnet p\u00e5 nettverket.", "no_sensors_or_port_mapping": "Aktiver minst sensorer eller port mapping", - "single_instance_allowed": "Bare en enkelt konfigurasjon av UPnP / IGD er n\u00f8dvendig." + "single_instance_allowed": "Bare en konfigurasjon av UPnP / IGD er n\u00f8dvendig." }, "error": { "few": "f\u00e5", diff --git a/homeassistant/components/upnp/.translations/zh-Hant.json b/homeassistant/components/upnp/.translations/zh-Hant.json index 2a036a1d2f3577..1611dac27213e3 100644 --- a/homeassistant/components/upnp/.translations/zh-Hant.json +++ b/homeassistant/components/upnp/.translations/zh-Hant.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "incomplete_device": "\u5ffd\u7565\u4e0d\u76f8\u5bb9 UPnP \u88dd\u7f6e", + "incomplete_device": "\u5ffd\u7565\u4e0d\u76f8\u5bb9 UPnP \u8a2d\u5099", "no_devices_discovered": "\u672a\u641c\u5c0b\u5230 UPnP/IGD", - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 UPnP/IGD \u88dd\u7f6e\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 UPnP/IGD \u8a2d\u5099\u3002", "no_sensors_or_port_mapping": "\u81f3\u5c11\u958b\u555f\u611f\u61c9\u5668\u6216\u901a\u8a0a\u57e0\u8f49\u767c", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 UPnP/IGD \u5373\u53ef\u3002" }, diff --git a/homeassistant/components/wemo/.translations/no.json b/homeassistant/components/wemo/.translations/no.json index 917eb0ef3a9d76..25a4172f00c092 100644 --- a/homeassistant/components/wemo/.translations/no.json +++ b/homeassistant/components/wemo/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av Wemo er mulig." + "single_instance_allowed": "Kun en konfigurasjon av Wemo er mulig." }, "step": { "confirm": { diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json index 71934516ea7f1b..7f32ade8a0892b 100644 --- a/homeassistant/components/withings/.translations/sl.json +++ b/homeassistant/components/withings/.translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_flows": "withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo." + "no_flows": "Withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo." }, "create_entry": { "default": "Uspe\u0161no overjen z Withings za izbrani profil." diff --git a/homeassistant/components/withings/.translations/zh-Hant.json b/homeassistant/components/withings/.translations/zh-Hant.json index 9e408eb0d5c19e..f01109b2d621d8 100644 --- a/homeassistant/components/withings/.translations/zh-Hant.json +++ b/homeassistant/components/withings/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Withings \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002\u8acb\u53c3\u95b1\u6587\u4ef6\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u8a2d\u5099\u3002" }, "step": { "user": { diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 36bdfb6d5d33a5..623c33637e0e9d 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Kun \u00e9n enkelt konfigurasjon av ZHA er tillatt." + "single_instance_allowed": "Kun en konfigurasjon av ZHA er tillatt." }, "error": { "cannot_connect": "Kan ikke koble til ZHA-enhet." @@ -44,11 +44,11 @@ }, "trigger_type": { "device_dropped": "Enheten ble brutt", - "device_flipped": "Enheten snudd \"{undertype}\"", - "device_knocked": "Enheten sl\u00e5tt \"{undertype}\"", - "device_rotated": "Enheten roterte \"{under type}\"", + "device_flipped": "Enheten snudd \"{subtype}\"", + "device_knocked": "Enheten sl\u00e5tt \"{subtype}\"", + "device_rotated": "Enheten roterte \"{subtype}\"", "device_shaken": "Enhet er ristet", - "device_slid": "Enheten skled \"{undertype}\"", + "device_slid": "Enheten skled \"{subtype}\"", "device_tilted": "Enhet vippet", "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", @@ -57,7 +57,7 @@ "remote_button_quintuple_press": "\" {subtype} \" - knappen ble femdobbelt klikket", "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", - "remote_button_triple_press": "\" {subtype} \"-knappen ble rippel klikket" + "remote_button_triple_press": "\"{subtype}\"-knappen ble trippel klikket" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/sv.json b/homeassistant/components/zha/.translations/sv.json index 818d041b4f1164..2762adc0fba19f 100644 --- a/homeassistant/components/zha/.translations/sv.json +++ b/homeassistant/components/zha/.translations/sv.json @@ -16,5 +16,17 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "close": "St\u00e4ng", + "dim_down": "Dimma ned", + "dim_up": "Dimma upp", + "left": "V\u00e4nster", + "open": "\u00d6ppen", + "right": "H\u00f6ger", + "turn_off": "St\u00e4ng av", + "turn_on": "Starta" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/zh-Hant.json b/homeassistant/components/zha/.translations/zh-Hant.json index e31e42bfbcfa18..bbfb3fe7128f27 100644 --- a/homeassistant/components/zha/.translations/zh-Hant.json +++ b/homeassistant/components/zha/.translations/zh-Hant.json @@ -4,17 +4,60 @@ "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 ZHA\u3002" }, "error": { - "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 ZHA \u88dd\u7f6e\u3002" + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 ZHA \u8a2d\u5099\u3002" }, "step": { "user": { "data": { "radio_type": "\u7121\u7dda\u96fb\u985e\u578b", - "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" + "usb_path": "USB \u8a2d\u5099\u8def\u5f91" }, "title": "ZHA" } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u5169\u500b\u6309\u9215", + "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", + "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "button_5": "\u7b2c\u4e94\u500b\u6309\u9215", + "button_6": "\u7b2c\u516d\u500b\u6309\u9215", + "close": "\u95dc\u9589", + "dim_down": "\u8abf\u6697", + "dim_up": "\u8abf\u4eae", + "face_1": "\u5df2\u7531\u9762\u5bb9 1 \u958b\u555f", + "face_2": "\u5df2\u7531\u9762\u5bb9 2 \u958b\u555f", + "face_3": "\u5df2\u7531\u9762\u5bb9 3 \u958b\u555f", + "face_4": "\u5df2\u7531\u9762\u5bb9 4 \u958b\u555f", + "face_5": "\u5df2\u7531\u9762\u5bb9 5 \u958b\u555f", + "face_6": "\u5df2\u7531\u9762\u5bb9 6 \u958b\u555f", + "face_any": "\u5df2\u7531\u4efb\u4f55/\u7279\u5b9a\u9762\u5bb9\u958b\u555f", + "left": "\u5de6", + "open": "\u958b\u555f", + "right": "\u53f3", + "turn_off": "\u95dc\u9589", + "turn_on": "\u958b\u555f" + }, + "trigger_type": { + "device_dropped": "\u8a2d\u5099\u6389\u843d", + "device_flipped": "\u7ffb\u52d5 \"{subtype}\" \u8a2d\u5099", + "device_knocked": "\u6572\u64ca \"{subtype}\" \u8a2d\u5099", + "device_rotated": "\u65cb\u8f49 \"{subtype}\" \u8a2d\u5099", + "device_shaken": "\u8a2d\u5099\u6416\u6643", + "device_slid": "\u63a8\u52d5 \"{subtype}\" \u8a2d\u5099", + "device_tilted": "\u8a2d\u5099\u540d\u7a31", + "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", + "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", + "remote_button_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e", + "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u64ca", + "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u64ca", + "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", + "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u64ca" + } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/no.json b/homeassistant/components/zwave/.translations/no.json index f70eaa4826014d..1d5584a82a053a 100644 --- a/homeassistant/components/zwave/.translations/no.json +++ b/homeassistant/components/zwave/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Z-Wave er allerede konfigurert", - "one_instance_only": "Komponenten st\u00f8tter kun en enkelt Z-Wave-forekomst" + "one_instance_only": "Komponenten st\u00f8tter kun en Z-Wave-forekomst" }, "error": { "option_error": "Z-Wave-validering mislyktes. Er banen til USB dongel riktig?" From 45c548ae47ab9cfedb060e9bb1a7f8b01fbea1bc Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 26 Sep 2019 22:53:26 -0700 Subject: [PATCH 175/296] Bump androidtv to 0.0.28 (#26906) * Bump androidtv to 0.0.28 * Address reviewer comments * Remove adb-shell from requirements_test_all.txt * Use a one-liner to avoid a coverage failure --- .../components/androidtv/manifest.json | 3 +- .../components/androidtv/media_player.py | 32 +++++----- requirements_all.txt | 5 +- requirements_test_all.txt | 2 +- tests/components/androidtv/patchers.py | 63 ++++++++++--------- .../components/androidtv/test_media_player.py | 8 ++- 6 files changed, 61 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 6643faa85bdeb3..c085addad4dd37 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/components/androidtv", "requirements": [ - "androidtv==0.0.27" + "adb-shell==0.0.2", + "androidtv==0.0.28" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index d68f47b1b0a3b7..fcf4950f5e2903 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -3,6 +3,12 @@ import logging import voluptuous as vol +from adb_shell.exceptions import ( + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + TcpTimeoutException, +) from androidtv import setup, ha_state_detection_rules_validator from androidtv.constants import APPS, KEYS @@ -123,11 +129,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Android TV / Fire TV platform.""" hass.data.setdefault(ANDROIDTV_DOMAIN, {}) - host = "{0}:{1}".format(config[CONF_HOST], config[CONF_PORT]) + host = f"{config[CONF_HOST]}:{config[CONF_PORT]}" if CONF_ADB_SERVER_IP not in config: - # Use "python-adb" (Python ADB implementation) - adb_log = "using Python ADB implementation " + # Use "adb_shell" (Python ADB implementation) + adb_log = "using Python ADB implementation " + ( + f"with adbkey='{config[CONF_ADBKEY]}'" + if CONF_ADBKEY in config + else "without adbkey authentication" + ) if CONF_ADBKEY in config: aftv = setup( host, @@ -135,7 +145,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) - adb_log += "with adbkey='{0}'".format(config[CONF_ADBKEY]) else: aftv = setup( @@ -143,7 +152,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) - adb_log += "without adbkey authentication" else: # Use "pure-python-adb" (communicate with ADB server) aftv = setup( @@ -153,9 +161,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) - adb_log = "using ADB server at {0}:{1}".format( - config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT] - ) + adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" if not aftv.available: # Determine the name that will be used for the device in the log @@ -251,6 +257,7 @@ def _adb_exception_catcher(self, *args, **kwargs): "establishing attempt in the next update. Error: %s", err, ) + self.aftv.adb.close() self._available = False # pylint: disable=protected-access return None @@ -278,14 +285,7 @@ def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): # ADB exceptions to catch if not self.aftv.adb_server_ip: - # Using "python-adb" (Python ADB implementation) - from adb.adb_protocol import ( - InvalidChecksumError, - InvalidCommandError, - InvalidResponseError, - ) - from adb.usb_exceptions import TcpTimeoutException - + # Using "adb_shell" (Python ADB implementation) self.exceptions = ( AttributeError, BrokenPipeError, diff --git a/requirements_all.txt b/requirements_all.txt index 19bdc4efd833ff..a9ec02ffcc62ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -111,6 +111,9 @@ adafruit-blinka==1.2.1 # homeassistant.components.mcp23017 adafruit-circuitpython-mcp230xx==1.1.2 +# homeassistant.components.androidtv +adb-shell==0.0.2 + # homeassistant.components.adguard adguardhome==0.2.1 @@ -197,7 +200,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.27 +androidtv==0.0.28 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a4fa60f15e697..c3f40e16349292 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -80,7 +80,7 @@ aiowwlln==2.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.27 +androidtv==0.0.28 # homeassistant.components.apns apns2==0.3.0 diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 86d1c1c15bdc02..73aa52259890ef 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -4,34 +4,24 @@ from unittest.mock import patch -class AdbCommandsFake: - """A fake of the `adb.adb_commands.AdbCommands` class.""" +class AdbDeviceFake: + """A fake of the `adb_shell.adb_device.AdbDevice` class.""" - def ConnectDevice(self, *args, **kwargs): # pylint: disable=invalid-name + def __init__(self, *args, **kwargs): + """Initialize a fake `adb_shell.adb_device.AdbDevice` instance.""" + self.available = False + + def close(self): + """Close the socket connection.""" + self.available = False + + def connect(self, *args, **kwargs): """Try to connect to a device.""" raise NotImplementedError - def Shell(self, cmd): # pylint: disable=invalid-name + def shell(self, cmd): """Send an ADB shell command.""" - raise NotImplementedError - - -class AdbCommandsFakeSuccess(AdbCommandsFake): - """A fake of the `adb.adb_commands.AdbCommands` class when the connection attempt succeeds.""" - - def ConnectDevice(self, *args, **kwargs): # pylint: disable=invalid-name - """Successfully connect to a device.""" - return self - - -class AdbCommandsFakeFail(AdbCommandsFake): - """A fake of the `adb.adb_commands.AdbCommands` class when the connection attempt fails.""" - - def ConnectDevice( - self, *args, **kwargs - ): # pylint: disable=invalid-name, no-self-use - """Fail to connect to a device.""" - raise socket_error + return None class ClientFakeSuccess: @@ -85,31 +75,39 @@ def shell(self, cmd): def patch_connect(success): - """Mock the `adb.adb_commands.AdbCommands` and `adb_messenger.client.Client` classes.""" + """Mock the `adb_shell.adb_device.AdbDevice` and `adb_messenger.client.Client` classes.""" + + def connect_success_python(self, *args, **kwargs): + """Mock the `AdbDeviceFake.connect` method when it succeeds.""" + self.available = True + + def connect_fail_python(self, *args, **kwargs): + """Mock the `AdbDeviceFake.connect` method when it fails.""" + raise socket_error if success: return { "python": patch( - "androidtv.adb_manager.AdbCommands", AdbCommandsFakeSuccess + f"{__name__}.AdbDeviceFake.connect", connect_success_python ), "server": patch("androidtv.adb_manager.Client", ClientFakeSuccess), } return { - "python": patch("androidtv.adb_manager.AdbCommands", AdbCommandsFakeFail), + "python": patch(f"{__name__}.AdbDeviceFake.connect", connect_fail_python), "server": patch("androidtv.adb_manager.Client", ClientFakeFail), } def patch_shell(response=None, error=False): - """Mock the `AdbCommandsFake.Shell` and `DeviceFake.shell` methods.""" + """Mock the `AdbDeviceFake.shell` and `DeviceFake.shell` methods.""" def shell_success(self, cmd): - """Mock the `AdbCommandsFake.Shell` and `DeviceFake.shell` methods when they are successful.""" + """Mock the `AdbDeviceFake.shell` and `DeviceFake.shell` methods when they are successful.""" self.shell_cmd = cmd return response def shell_fail_python(self, cmd): - """Mock the `AdbCommandsFake.Shell` method when it fails.""" + """Mock the `AdbDeviceFake.shell` method when it fails.""" self.shell_cmd = cmd raise AttributeError @@ -120,10 +118,13 @@ def shell_fail_server(self, cmd): if not error: return { - "python": patch(f"{__name__}.AdbCommandsFake.Shell", shell_success), + "python": patch(f"{__name__}.AdbDeviceFake.shell", shell_success), "server": patch(f"{__name__}.DeviceFake.shell", shell_success), } return { - "python": patch(f"{__name__}.AdbCommandsFake.Shell", shell_fail_python), + "python": patch(f"{__name__}.AdbDeviceFake.shell", shell_fail_python), "server": patch(f"{__name__}.DeviceFake.shell", shell_fail_server), } + + +PATCH_ADB_DEVICE = patch("androidtv.adb_manager.AdbDevice", AdbDeviceFake) diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 39b392c97ee306..feffc70d84148f 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -79,7 +79,9 @@ async def _test_reconnect(hass, caplog, config): else: entity_id = "media_player.fire_tv" - with patchers.patch_connect(True)[patch_key], patchers.patch_shell("")[patch_key]: + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, config) await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -151,7 +153,9 @@ async def _test_adb_shell_returns_none(hass, config): else: entity_id = "media_player.fire_tv" - with patchers.patch_connect(True)[patch_key], patchers.patch_shell("")[patch_key]: + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, config) await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) From 77b7e4665bc16cce52b6711d90d4660527d18f13 Mon Sep 17 00:00:00 2001 From: Oleksandr Omelchuk <25319+sashao@users.noreply.github.com> Date: Fri, 27 Sep 2019 08:54:40 +0300 Subject: [PATCH 176/296] Add more ebusd Vaillant "bai" sensors (#26750) * Added support for more ebus "bai" sensors. * Using common constants for measuring unit names. Fixed review item * dummy commit to restart CI * Fixed review item * Fixed formatting black --fast homeassistant * Removed trailing spaces * Fixed black formatting * trigger new build --- homeassistant/components/ebusd/const.py | 68 ++++++++++++++++++------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index 7587d0cd42dddb..db79d81736ef75 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -1,40 +1,41 @@ """Constants for ebus component.""" -from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, PRESSURE_BAR DOMAIN = "ebusd" +TIME_SECONDS = "seconds" -# SensorTypes: +# SensorTypes from ebusdpy module : # 0='decimal', 1='time-schedule', 2='switch', 3='string', 4='value;status' SENSOR_TYPES = { "700": { "ActualFlowTemperatureDesired": [ "Hc1ActualFlowTempDesired", - "°C", + TEMP_CELSIUS, "mdi:thermometer", 0, ], "MaxFlowTemperatureDesired": [ "Hc1MaxFlowTempDesired", - "°C", + TEMP_CELSIUS, "mdi:thermometer", 0, ], "MinFlowTemperatureDesired": [ "Hc1MinFlowTempDesired", - "°C", + TEMP_CELSIUS, "mdi:thermometer", 0, ], "PumpStatus": ["Hc1PumpStatus", None, "mdi:toggle-switch", 2], "HCSummerTemperatureLimit": [ "Hc1SummerTempLimit", - "°C", + TEMP_CELSIUS, "mdi:weather-sunny", 0, ], - "HolidayTemperature": ["HolidayTemp", "°C", "mdi:thermometer", 0], - "HWTemperatureDesired": ["HwcTempDesired", "°C", "mdi:thermometer", 0], + "HolidayTemperature": ["HolidayTemp", TEMP_CELSIUS, "mdi:thermometer", 0], + "HWTemperatureDesired": ["HwcTempDesired", TEMP_CELSIUS, "mdi:thermometer", 0], "HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer", 1], "HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer", 1], "HWTimerWednesday": ["hwcTimer.Wednesday", None, "mdi:timer", 1], @@ -42,15 +43,20 @@ "HWTimerFriday": ["hwcTimer.Friday", None, "mdi:timer", 1], "HWTimerSaturday": ["hwcTimer.Saturday", None, "mdi:timer", 1], "HWTimerSunday": ["hwcTimer.Sunday", None, "mdi:timer", 1], - "WaterPressure": ["WaterPressure", "bar", "mdi:water-pump", 0], + "WaterPressure": ["WaterPressure", PRESSURE_BAR, "mdi:water-pump", 0], "Zone1RoomZoneMapping": ["z1RoomZoneMapping", None, "mdi:label", 0], - "Zone1NightTemperature": ["z1NightTemp", "°C", "mdi:weather-night", 0], - "Zone1DayTemperature": ["z1DayTemp", "°C", "mdi:weather-sunny", 0], - "Zone1HolidayTemperature": ["z1HolidayTemp", "°C", "mdi:thermometer", 0], - "Zone1RoomTemperature": ["z1RoomTemp", "°C", "mdi:thermometer", 0], + "Zone1NightTemperature": ["z1NightTemp", TEMP_CELSIUS, "mdi:weather-night", 0], + "Zone1DayTemperature": ["z1DayTemp", TEMP_CELSIUS, "mdi:weather-sunny", 0], + "Zone1HolidayTemperature": [ + "z1HolidayTemp", + TEMP_CELSIUS, + "mdi:thermometer", + 0, + ], + "Zone1RoomTemperature": ["z1RoomTemp", TEMP_CELSIUS, "mdi:thermometer", 0], "Zone1ActualRoomTemperatureDesired": [ "z1ActualRoomTempDesired", - "°C", + TEMP_CELSIUS, "mdi:thermometer", 0, ], @@ -62,7 +68,7 @@ "Zone1TimerSaturday": ["z1Timer.Saturday", None, "mdi:timer", 1], "Zone1TimerSunday": ["z1Timer.Sunday", None, "mdi:timer", 1], "Zone1OperativeMode": ["z1OpMode", None, "mdi:math-compass", 3], - "ContinuosHeating": ["ContinuosHeating", "°C", "mdi:weather-snowy", 0], + "ContinuosHeating": ["ContinuosHeating", TEMP_CELSIUS, "mdi:weather-snowy", 0], "PowerEnergyConsumptionLastMonth": [ "PrEnergySumHcLastMonth", ENERGY_KILO_WATT_HOUR, @@ -77,14 +83,38 @@ ], }, "ehp": { - "HWTemperature": ["HwcTemp", "°C", "mdi:thermometer", 4], - "OutsideTemp": ["OutsideTemp", "°C", "mdi:thermometer", 4], + "HWTemperature": ["HwcTemp", TEMP_CELSIUS, "mdi:thermometer", 4], + "OutsideTemp": ["OutsideTemp", TEMP_CELSIUS, "mdi:thermometer", 4], }, "bai": { - "ReturnTemperature": ["ReturnTemp", "°C", "mdi:thermometer", 4], + "HotWaterTemperature": ["HwcTemp", TEMP_CELSIUS, "mdi:thermometer", 4], + "StorageTemperature": ["StorageTemp", TEMP_CELSIUS, "mdi:thermometer", 4], + "DesiredStorageTemperature": [ + "StorageTempDesired", + TEMP_CELSIUS, + "mdi:thermometer", + 0, + ], + "OutdoorsTemperature": [ + "OutdoorstempSensor", + TEMP_CELSIUS, + "mdi:thermometer", + 4, + ], + "WaterPreasure": ["WaterPressure", PRESSURE_BAR, "mdi:pipe", 4], + "AverageIgnitionTime": ["averageIgnitiontime", TIME_SECONDS, "mdi:av-timer", 0], + "MaximumIgnitionTime": ["maxIgnitiontime", TIME_SECONDS, "mdi:av-timer", 0], + "MinimumIgnitionTime": ["minIgnitiontime", TIME_SECONDS, "mdi:av-timer", 0], + "ReturnTemperature": ["ReturnTemp", TEMP_CELSIUS, "mdi:thermometer", 4], "CentralHeatingPump": ["WP", None, "mdi:toggle-switch", 2], "HeatingSwitch": ["HeatingSwitch", None, "mdi:toggle-switch", 2], - "FlowTemperature": ["FlowTemp", "°C", "mdi:thermometer", 4], + "DesiredFlowTemperature": [ + "FlowTempDesired", + TEMP_CELSIUS, + "mdi:thermometer", + 0, + ], + "FlowTemperature": ["FlowTemp", TEMP_CELSIUS, "mdi:thermometer", 4], "Flame": ["Flame", None, "mdi:toggle-switch", 2], "PowerEnergyConsumptionHeatingCircuit": [ "PrEnergySumHc1", From 9f6fade2365ee43a686c03d83cef4be0f1f69cfd Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 27 Sep 2019 08:02:58 +0200 Subject: [PATCH 177/296] Add xbox live custom update interval (#26939) * Add xbox_live custom update schedule * Set a default update interval that is within the free limit. Consider number of users per api key when setting interval. * Add codeowner * Add callback decorator --- CODEOWNERS | 1 + .../components/xbox_live/manifest.json | 4 +- homeassistant/components/xbox_live/sensor.py | 102 +++++++++++------- 3 files changed, 68 insertions(+), 39 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 419bc1a8606abb..4a6dfdbf6e62ab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -318,6 +318,7 @@ homeassistant/components/wemo/* @sqldiablo homeassistant/components/withings/* @vangorra homeassistant/components/worldclock/* @fabaff homeassistant/components/wwlln/* @bachya +homeassistant/components/xbox_live/* @MartinHjelmare homeassistant/components/xfinity/* @cisasteelersfan homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi homeassistant/components/xiaomi_miio/* @rytilahti @syssi diff --git a/homeassistant/components/xbox_live/manifest.json b/homeassistant/components/xbox_live/manifest.json index 0d80ce770ced7c..5baf928352d514 100644 --- a/homeassistant/components/xbox_live/manifest.json +++ b/homeassistant/components/xbox_live/manifest.json @@ -6,5 +6,7 @@ "xboxapi==0.1.1" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@MartinHjelmare" + ] } diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index d7ca22b2be3c33..e837cc4bbbc0fe 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -1,12 +1,16 @@ """Sensor for Xbox Live account status.""" import logging +from datetime import timedelta import voluptuous as vol +from xboxapi import xbox_api -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, STATE_UNKNOWN +from homeassistant.const import CONF_API_KEY, CONF_SCAN_INTERVAL +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -24,65 +28,76 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Xbox platform.""" - from xboxapi import xbox_api - - api = xbox_api.XboxApi(config.get(CONF_API_KEY)) - devices = [] + api = xbox_api.XboxApi(config[CONF_API_KEY]) + entities = [] # request personal profile to check api connection profile = api.get_profile() if profile.get("error_code") is not None: _LOGGER.error( "Can't setup XboxAPI connection. Check your account or " - " api key on xboxapi.com. Code: %s Description: %s ", - profile.get("error_code", STATE_UNKNOWN), - profile.get("error_message", STATE_UNKNOWN), + "api key on xboxapi.com. Code: %s Description: %s ", + profile.get("error_code", "unknown"), + profile.get("error_message", "unknown"), ) return - for xuid in config.get(CONF_XUID): - new_device = XboxSensor(hass, api, xuid) - if new_device.success_init: - devices.append(new_device) + users = config[CONF_XUID] + + interval = timedelta(minutes=1 * len(users)) + interval = config.get(CONF_SCAN_INTERVAL, interval) + + for xuid in users: + gamercard = get_user_gamercard(api, xuid) + if gamercard is None: + continue + entities.append(XboxSensor(api, xuid, gamercard, interval)) + + if entities: + add_entities(entities, True) + - if devices: - add_entities(devices, True) +def get_user_gamercard(api, xuid): + """Get profile info.""" + gamercard = api.get_user_gamercard(xuid) + _LOGGER.debug("User gamercard: %s", gamercard) + + if gamercard.get("success", True) and gamercard.get("code") is None: + return gamercard + _LOGGER.error( + "Can't get user profile %s. Error Code: %s Description: %s", + xuid, + gamercard.get("code", "unknown"), + gamercard.get("description", "unknown"), + ) + return None class XboxSensor(Entity): """A class for the Xbox account.""" - def __init__(self, hass, api, xuid): + def __init__(self, api, xuid, gamercard, interval): """Initialize the sensor.""" - self._hass = hass self._state = None - self._presence = {} + self._presence = [] self._xuid = xuid self._api = api - - # get profile info - profile = self._api.get_user_gamercard(self._xuid) - - if profile.get("success", True) and profile.get("code") is None: - self.success_init = True - self._gamertag = profile.get("gamertag") - self._gamerscore = profile.get("gamerscore") - self._picture = profile.get("gamerpicSmallSslImagePath") - self._tier = profile.get("tier") - else: - _LOGGER.error( - "Can't get user profile %s. " "Error Code: %s Description: %s", - self._xuid, - profile.get("code", STATE_UNKNOWN), - profile.get("description", STATE_UNKNOWN), - ) - self.success_init = False + self._gamertag = gamercard.get("gamertag") + self._gamerscore = gamercard.get("gamerscore") + self._interval = interval + self._picture = gamercard.get("gamerpicSmallSslImagePath") + self._tier = gamercard.get("tier") @property def name(self): """Return the name of the sensor.""" return self._gamertag + @property + def should_poll(self): + """Return False as this entity has custom polling.""" + return False + @property def state(self): """Return the state of the sensor.""" @@ -98,7 +113,7 @@ def device_state_attributes(self): for device in self._presence: for title in device.get("titles"): attributes[ - "{} {}".format(device.get("type"), title.get("placement")) + f'{device.get("type")} {title.get("placement")}' ] = title.get("name") return attributes @@ -113,8 +128,19 @@ def icon(self): """Return the icon to use in the frontend.""" return ICON + async def async_added_to_hass(self): + """Start custom polling.""" + + @callback + def async_update(event_time=None): + """Update the entity.""" + self.async_schedule_update_ha_state(True) + + async_track_time_interval(self.hass, async_update, self._interval) + def update(self): """Update state data from Xbox API.""" presence = self._api.get_user_presence(self._xuid) + _LOGGER.debug("User presence: %s", presence) self._state = presence.get("state") - self._presence = presence.get("devices", {}) + self._presence = presence.get("devices", []) From fc700c79377e97f4e9f950f5c1d2707701fd5ac7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Sep 2019 23:49:51 -0700 Subject: [PATCH 178/296] Guard against non supported entities (#26941) --- homeassistant/components/alexa/state_report.py | 8 ++++++++ tests/components/alexa/test_state_report.py | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index b7ff9d17fe8214..42c16919a45cf1 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -131,6 +131,10 @@ async def async_send_add_or_update_message(hass, config, entity_ids): for entity_id in entity_ids: domain = entity_id.split(".", 1)[0] + + if domain not in ENTITY_ADAPTERS: + continue + alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) endpoints.append(alexa_entity.serialize_discovery()) @@ -161,6 +165,10 @@ async def async_send_delete_message(hass, config, entity_ids): for entity_id in entity_ids: domain = entity_id.split(".", 1)[0] + + if domain not in ENTITY_ADAPTERS: + continue + alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) endpoints.append({"endpointId": alexa_entity.alexa_id()}) diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 2b3f9f34adf57d..c05eed2a89baac 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -48,7 +48,7 @@ async def test_send_add_or_update_message(hass, aioclient_mock): ) await state_report.async_send_add_or_update_message( - hass, DEFAULT_CONFIG, ["binary_sensor.test_contact"] + hass, DEFAULT_CONFIG, ["binary_sensor.test_contact", "zwave.bla"] ) assert len(aioclient_mock.mock_calls) == 1 @@ -75,7 +75,7 @@ async def test_send_delete_message(hass, aioclient_mock): ) await state_report.async_send_delete_message( - hass, DEFAULT_CONFIG, ["binary_sensor.test_contact"] + hass, DEFAULT_CONFIG, ["binary_sensor.test_contact", "zwave.bla"] ) assert len(aioclient_mock.mock_calls) == 1 From 454d479b36b314e3d2308f8ef8571ae38cafa4ad Mon Sep 17 00:00:00 2001 From: jaburges Date: Fri, 27 Sep 2019 03:52:58 -0700 Subject: [PATCH 179/296] Bump pyowlet to 1.0.3 (#26892) * API URLs API URLs have been updated which prevented 1.0.2 from authenticating per this thread: https://community.home-assistant.io/t/owlet-setup/130751 ``` https://user-field-1a2039d9.aylanetworks.com/users/sign_in https://ads-field-1a2039d9.aylanetworks.com/apiv1 ``` have been updated in PyOwlet.py 1.0.3 * bump pyowlet to 1.0.3 --- homeassistant/components/owlet/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/owlet/manifest.json b/homeassistant/components/owlet/manifest.json index edc51dcc5333ab..b89947343c94f4 100644 --- a/homeassistant/components/owlet/manifest.json +++ b/homeassistant/components/owlet/manifest.json @@ -3,7 +3,7 @@ "name": "Owlet", "documentation": "https://www.home-assistant.io/components/owlet", "requirements": [ - "pyowlet==1.0.2" + "pyowlet==1.0.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index a9ec02ffcc62ae..32983063775dcd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1372,7 +1372,7 @@ pyotgw==0.4b4 pyotp==2.3.0 # homeassistant.components.owlet -pyowlet==1.0.2 +pyowlet==1.0.3 # homeassistant.components.openweathermap pyowm==2.10.0 From b88772eea0bacea18d2a491d7ecba1303f628152 Mon Sep 17 00:00:00 2001 From: joe248 <24720046+joe248@users.noreply.github.com> Date: Fri, 27 Sep 2019 08:31:01 -0500 Subject: [PATCH 180/296] Revert Nest HVAC mode when disabling Eco mode (#26934) --- homeassistant/components/nest/climate.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index dc4b0bd33ae823..eec7108cdeabd9 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -192,7 +192,10 @@ def current_temperature(self): def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" if self._mode == NEST_MODE_ECO: - # We assume the first operation in operation list is the main one + if self.device.previous_mode in MODE_NEST_TO_HASS: + return MODE_NEST_TO_HASS[self.device.previous_mode] + + # previous_mode not supported so return the first compatible mode return self._operation_list[0] return MODE_NEST_TO_HASS[self._mode] @@ -270,7 +273,7 @@ def preset_mode(self): if self._mode == NEST_MODE_ECO: return PRESET_ECO - return None + return PRESET_NONE @property def preset_modes(self): @@ -294,7 +297,7 @@ def set_preset_mode(self, preset_mode): if need_eco: self.device.mode = NEST_MODE_ECO else: - self.device.mode = MODE_HASS_TO_NEST[self._operation_list[0]] + self.device.mode = self.device.previous_mode @property def fan_mode(self): From 62d515fa035fe621eb854f7074f16d514634d8be Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 27 Sep 2019 16:26:06 +0200 Subject: [PATCH 181/296] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 29e68a5d7acc16..31908166dda0d9 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -233,3 +233,43 @@ stages: fi displayName: 'Create Meta-Image' + +- stage: 'Addidional' + jobs: + - job: 'Updater' + pool: + vmImage: 'ubuntu-latest' + variables: + - group: gcloud + steps: + - template: templates/azp-step-ha-version.yaml@azure + - script: | + set -e + + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + + curl -o google-cloud-sdk.tar.gz https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz + tar -C . -xvf google-cloud-sdk.tar.gz + rm -f google-cloud-sdk.tar.gz + + ./google-cloud-sdk/install.sh + displayName: 'Setup gCloud' + condition: eq(variables['homeassistantReleaseStable'], 'true')) + - script: | + set -e + + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + + echo "$(gcloudAuth)" > gcloud.key + ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud.key + rm -f gcloud.key + displayName: 'Auth gCloud' + condition: eq(variables['homeassistantReleaseStable'], 'true')) + - script: | + set -e + + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + + ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver --update-env-vars VERSION=$(homeassistantRelease) + displayName: 'Push details to updater' + condition: eq(variables['homeassistantReleaseStable'], 'true')) From e989239e191c8a368168cf4bc99e7095589e4a43 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 27 Sep 2019 16:43:23 +0200 Subject: [PATCH 182/296] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 31908166dda0d9..626c056cef81f7 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -260,9 +260,9 @@ stages: export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - echo "$(gcloudAuth)" > gcloud.key - ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud.key - rm -f gcloud.key + echo "$(gcloudAuth)" > gcloud_auth.json + ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud_auth.json + rm -f gcloud_auth.json displayName: 'Auth gCloud' condition: eq(variables['homeassistantReleaseStable'], 'true')) - script: | From 1de002013fbb11bc2dc988743a5c17a81fa6cdbf Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Fri, 27 Sep 2019 10:45:50 -0400 Subject: [PATCH 183/296] Fix ecobee integration (#26951) * Check for DATA_ECOBEE_CONFIG * Update homeassistant/components/ecobee/config_flow.py Co-Authored-By: Martin Hjelmare --- homeassistant/components/ecobee/config_flow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py index f4cd4fc5bf086d..56ce13f7701b99 100644 --- a/homeassistant/components/ecobee/config_flow.py +++ b/homeassistant/components/ecobee/config_flow.py @@ -33,7 +33,11 @@ async def async_step_user(self, user_input=None): return self.async_abort(reason="one_instance_only") errors = {} - stored_api_key = self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + stored_api_key = ( + self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + if DATA_ECOBEE_CONFIG in self.hass.data + else "" + ) if user_input is not None: # Use the user-supplied API key to attempt to obtain a PIN from ecobee. From 4d92e76f19ab2729cbedb1cac891c4a4f38e89ff Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 27 Sep 2019 16:59:07 +0200 Subject: [PATCH 184/296] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 40 ------------------------------------- 1 file changed, 40 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 626c056cef81f7..29e68a5d7acc16 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -233,43 +233,3 @@ stages: fi displayName: 'Create Meta-Image' - -- stage: 'Addidional' - jobs: - - job: 'Updater' - pool: - vmImage: 'ubuntu-latest' - variables: - - group: gcloud - steps: - - template: templates/azp-step-ha-version.yaml@azure - - script: | - set -e - - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - curl -o google-cloud-sdk.tar.gz https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz - tar -C . -xvf google-cloud-sdk.tar.gz - rm -f google-cloud-sdk.tar.gz - - ./google-cloud-sdk/install.sh - displayName: 'Setup gCloud' - condition: eq(variables['homeassistantReleaseStable'], 'true')) - - script: | - set -e - - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - echo "$(gcloudAuth)" > gcloud_auth.json - ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud_auth.json - rm -f gcloud_auth.json - displayName: 'Auth gCloud' - condition: eq(variables['homeassistantReleaseStable'], 'true')) - - script: | - set -e - - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver --update-env-vars VERSION=$(homeassistantRelease) - displayName: 'Push details to updater' - condition: eq(variables['homeassistantReleaseStable'], 'true')) From 588bc2666112f9d939c1fc47a77ea5eceafef8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Fri, 27 Sep 2019 17:42:32 +0200 Subject: [PATCH 185/296] Add CO2 level reading for Kaiterra integration (#26935) --- homeassistant/components/kaiterra/air_quality.py | 5 +++++ homeassistant/components/kaiterra/api_data.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py index 4dfe04f9c2e12d..70699de394cf5c 100644 --- a/homeassistant/components/kaiterra/air_quality.py +++ b/homeassistant/components/kaiterra/air_quality.py @@ -82,6 +82,11 @@ def particulate_matter_10(self): """Return the particulate matter 10 level.""" return self._data("rpm10c") + @property + def carbon_dioxide(self): + """Return the CO2 (carbon dioxide) level.""" + return self._data("rco2") + @property def volatile_organic_compounds(self): """Return the VOC (Volatile Organic Compounds) level.""" diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py index 0c2d6d9366147d..81e28438d567f8 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -23,7 +23,7 @@ _LOGGER = getLogger(__name__) -POLLUTANTS = {"rpm25c": "PM2.5", "rpm10c": "PM10", "rtvoc": "TVOC"} +POLLUTANTS = {"rpm25c": "PM2.5", "rpm10c": "PM10", "rtvoc": "TVOC", "rco2": "CO2"} class KaiterraApiData: From e57e7e844921e06fb629357dbe2a638989c2db24 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 Sep 2019 17:48:48 +0200 Subject: [PATCH 186/296] Improve validation of device trigger config (#26910) * Improve validation of device trigger config * Remove action and condition checks * Move config validation to own file * Fix tests * Fixes * Fixes * Small tweak --- homeassistant/components/automation/config.py | 60 ++++++++++++++++++ homeassistant/components/automation/device.py | 20 +++--- homeassistant/components/config/__init__.py | 17 +++-- homeassistant/components/config/automation.py | 2 + .../components/device_automation/__init__.py | 23 +++++++ homeassistant/config.py | 43 +++++++++---- homeassistant/helpers/config_validation.py | 2 +- homeassistant/loader.py | 2 +- .../components/device_automation/test_init.py | 62 +++++++++++++++++++ tests/helpers/test_check_config.py | 4 +- tests/scripts/test_check_config.py | 4 +- 11 files changed, 209 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/automation/config.py diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py new file mode 100644 index 00000000000000..04b764e271ca48 --- /dev/null +++ b/homeassistant/components/automation/config.py @@ -0,0 +1,60 @@ +"""Config validation helper for the automation integration.""" +import asyncio +import importlib + +import voluptuous as vol + +from homeassistant.const import CONF_PLATFORM +from homeassistant.config import async_log_exception, config_without_domain +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform +from homeassistant.loader import IntegrationNotFound + +from . import CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA + +# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs, no-warn-return-any + + +async def async_validate_config_item(hass, config, full_config=None): + """Validate config item.""" + try: + config = PLATFORM_SCHEMA(config) + + triggers = [] + for trigger in config[CONF_TRIGGER]: + trigger_platform = importlib.import_module( + "..{}".format(trigger[CONF_PLATFORM]), __name__ + ) + if hasattr(trigger_platform, "async_validate_trigger_config"): + trigger = await trigger_platform.async_validate_trigger_config( + hass, trigger + ) + triggers.append(trigger) + config[CONF_TRIGGER] = triggers + except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: + async_log_exception(ex, DOMAIN, full_config or config, hass) + return None + + return config + + +async def async_validate_config(hass, config): + """Validate config.""" + automations = [] + validated_automations = await asyncio.gather( + *( + async_validate_config_item(hass, p_config, config) + for _, p_config in config_per_platform(config, DOMAIN) + ) + ) + for validated_automation in validated_automations: + if validated_automation is not None: + automations.append(validated_automation) + + # Create a copy of the configuration with all config for current + # component removed and add validated config back in. + config = config_without_domain(config, DOMAIN) + config[DOMAIN] = automations + + return config diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index fe2d65edef616b..eb3e5a95c9c17e 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -1,20 +1,24 @@ """Offer device oriented automation.""" import voluptuous as vol -from homeassistant.const import CONF_DOMAIN, CONF_PLATFORM -from homeassistant.loader import async_get_integration +from homeassistant.components.device_automation import ( + TRIGGER_BASE_SCHEMA, + async_get_device_automation_platform, +) # mypy: allow-untyped-defs, no-check-untyped-defs -TRIGGER_SCHEMA = vol.Schema( - {vol.Required(CONF_PLATFORM): "device", vol.Required(CONF_DOMAIN): str}, - extra=vol.ALLOW_EXTRA, -) +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + + +async def async_validate_trigger_config(hass, config): + """Validate config.""" + platform = await async_get_device_automation_platform(hass, config, "trigger") + return platform.TRIGGER_SCHEMA(config) async def async_attach_trigger(hass, config, action, automation_info): """Listen for trigger.""" - integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_trigger") + platform = await async_get_device_automation_platform(hass, config, "trigger") return await platform.async_attach_trigger(hass, config, action, automation_info) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 6d4b465fceb3dc..569d1de6a02ce8 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -5,10 +5,11 @@ import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import ATTR_COMPONENT -from homeassistant.components.http import HomeAssistantView from homeassistant.util.yaml import load_yaml, dump DOMAIN = "config" @@ -80,6 +81,7 @@ def __init__( data_schema, *, post_write_hook=None, + data_validator=None, ): """Initialize a config view.""" self.url = f"/api/config/{component}/{config_type}/{{config_key}}" @@ -88,6 +90,7 @@ def __init__( self.key_schema = key_schema self.data_schema = data_schema self.post_write_hook = post_write_hook + self.data_validator = data_validator def _empty_config(self): """Empty config if file not found.""" @@ -128,14 +131,18 @@ async def post(self, request, config_key): except vol.Invalid as err: return self.json_message(f"Key malformed: {err}", 400) + hass = request.app["hass"] + try: # We just validate, we don't store that data because # we don't want to store the defaults. - self.data_schema(data) - except vol.Invalid as err: + if self.data_validator: + await self.data_validator(hass, data) + else: + self.data_schema(data) + except (vol.Invalid, HomeAssistantError) as err: return self.json_message(f"Message malformed: {err}", 400) - hass = request.app["hass"] path = hass.config.path(self.path) current = await self.read_config(hass) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 17efdba3fb573f..97ddf1e0714e69 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -3,6 +3,7 @@ import uuid from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.automation.config import async_validate_config_item from homeassistant.const import CONF_ID, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv @@ -26,6 +27,7 @@ async def hook(hass): cv.string, PLATFORM_SCHEMA, post_write_hook=hook, + data_validator=async_validate_config_item, ) ) return True diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index b444abd5238db4..62d338ece54a49 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -9,6 +9,8 @@ from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.loader import async_get_integration, IntegrationNotFound +from .exceptions import InvalidDeviceAutomationConfig + DOMAIN = "device_automation" _LOGGER = logging.getLogger(__name__) @@ -43,6 +45,27 @@ async def async_setup(hass, config): return True +async def async_get_device_automation_platform(hass, config, automation_type): + """Load device automation platform for integration. + + Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. + """ + platform_name, _ = TYPES[automation_type] + try: + integration = await async_get_integration(hass, config[CONF_DOMAIN]) + platform = integration.get_platform(platform_name) + except IntegrationNotFound: + raise InvalidDeviceAutomationConfig( + f"Integration '{config[CONF_DOMAIN]}' not found" + ) + except ImportError: + raise InvalidDeviceAutomationConfig( + f"Integration '{config[CONF_DOMAIN]}' does not support device automation {automation_type}s" + ) + + return platform + + async def _async_get_device_automations_from_domain( hass, domain, automation_type, device_id ): diff --git a/homeassistant/config.py b/homeassistant/config.py index d3bd97dad8f777..0e840e1d003062 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -416,7 +416,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: @callback def async_log_exception( - ex: vol.Invalid, domain: str, config: Dict, hass: HomeAssistant + ex: Exception, domain: str, config: Dict, hass: HomeAssistant ) -> None: """Log an error for configuration validation. @@ -428,23 +428,26 @@ def async_log_exception( @callback -def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: +def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: """Generate log exception for configuration validation. This method must be run in the event loop. """ message = f"Invalid config for [{domain}]: " - if "extra keys not allowed" in ex.error_message: - message += ( - "[{option}] is an invalid option for [{domain}]. " - "Check: {domain}->{path}.".format( - option=ex.path[-1], - domain=domain, - path="->".join(str(m) for m in ex.path), + if isinstance(ex, vol.Invalid): + if "extra keys not allowed" in ex.error_message: + message += ( + "[{option}] is an invalid option for [{domain}]. " + "Check: {domain}->{path}.".format( + option=ex.path[-1], + domain=domain, + path="->".join(str(m) for m in ex.path), + ) ) - ) + else: + message += "{}.".format(humanize_error(config, ex)) else: - message += "{}.".format(humanize_error(config, ex)) + message += str(ex) try: domain_config = config.get(domain, config) @@ -717,6 +720,24 @@ async def async_process_component_config( _LOGGER.error("Unable to import %s: %s", domain, ex) return None + # Check if the integration has a custom config validator + config_validator = None + try: + config_validator = integration.get_platform("config") + except ImportError: + pass + if config_validator is not None and hasattr( + config_validator, "async_validate_config" + ): + try: + return await config_validator.async_validate_config( # type: ignore + hass, config + ) + except (vol.Invalid, HomeAssistantError) as ex: + async_log_exception(ex, domain, config, hass) + return None + + # No custom config validator, proceed with schema validation if hasattr(component, "CONFIG_SCHEMA"): try: return component.CONFIG_SCHEMA(config) # type: ignore diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 113f2437ce8cea..d567962e328a8e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -90,7 +90,7 @@ def validate(obj: Dict) -> Dict: for k in obj.keys(): if k in keys: return obj - raise vol.Invalid("must contain one of {}.".format(", ".join(keys))) + raise vol.Invalid("must contain at least one of {}.".format(", ".join(keys))) return validate diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 1a9a3d256acb2d..272271eefb31af 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -307,7 +307,7 @@ class IntegrationNotFound(LoaderError): def __init__(self, domain: str) -> None: """Initialize a component not found error.""" - super().__init__(f"Integration {domain} not found.") + super().__init__(f"Integration '{domain}' not found.") self.domain = domain diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index b05c04a16f1dc5..6dcd8391bf8954 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -2,6 +2,7 @@ import pytest from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.helpers import device_registry @@ -161,3 +162,64 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert msg["success"] triggers = msg["result"] assert _same_lists(triggers, expected_triggers) + + +async def test_automation_with_non_existing_integration(hass, caplog): + """Test device automation with non existing integration.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": "none", + "domain": "beer", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "Integration 'beer' not found" in caplog.text + + +async def test_automation_with_integration_without_device_trigger(hass, caplog): + """Test automation with integration without device trigger support.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": "none", + "domain": "test", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert ( + "Integration 'test' does not support device automation triggers" in caplog.text + ) + + +async def test_automation_with_bad_trigger(hass, caplog): + """Test automation with bad device trigger.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "device", "domain": "light"}, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "required key not provided" in caplog.text diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 9e5ea15293a484..5228f0d4882228 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -75,7 +75,7 @@ async def test_component_platform_not_found(hass, loop): assert res.keys() == {"homeassistant"} assert res.errors[0] == CheckConfigError( - "Component error: beer - Integration beer not found.", None, None + "Component error: beer - Integration 'beer' not found.", None, None ) # Only 1 error expected @@ -95,7 +95,7 @@ async def test_component_platform_not_found_2(hass, loop): assert res["light"] == [] assert res.errors[0] == CheckConfigError( - "Platform error light.beer - Integration beer not found.", None, None + "Platform error light.beer - Integration 'beer' not found.", None, None ) # Only 1 error expected diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index bd4f37bd1357c3..18143c088be58a 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -63,7 +63,7 @@ def test_component_platform_not_found(isfile_patch, loop): assert res["components"].keys() == {"homeassistant"} assert res["except"] == { check_config.ERROR_STR: [ - "Component error: beer - Integration beer not found." + "Component error: beer - Integration 'beer' not found." ] } assert res["secret_cache"] == {} @@ -77,7 +77,7 @@ def test_component_platform_not_found(isfile_patch, loop): assert res["components"]["light"] == [] assert res["except"] == { check_config.ERROR_STR: [ - "Platform error light.beer - Integration beer not found." + "Platform error light.beer - Integration 'beer' not found." ] } assert res["secret_cache"] == {} From f267b37105b97d7574b37af6c33c03baec9a5f54 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 27 Sep 2019 17:58:16 +0200 Subject: [PATCH 187/296] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 29e68a5d7acc16..51c5cdb936d2f9 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -233,3 +233,36 @@ stages: fi displayName: 'Create Meta-Image' + +- stage: 'Addidional' + jobs: + - job: 'Updater' + pool: + vmImage: 'ubuntu-latest' + variables: + - group: gcloud + steps: + - template: templates/azp-step-ha-version.yaml@azure + - script: | + set -e + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + curl -o google-cloud-sdk.tar.gz https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz + tar -C . -xvf google-cloud-sdk.tar.gz + rm -f google-cloud-sdk.tar.gz + ./google-cloud-sdk/install.sh + displayName: 'Setup gCloud' + condition: eq(variables['homeassistantReleaseStable'], 'true')) + - script: | + set -e + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + echo "$(gcloudAuth)" > gcloud_auth.json + ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud_auth.json + rm -f gcloud_auth.json + displayName: 'Auth gCloud' + condition: eq(variables['homeassistantReleaseStable'], 'true')) + - script: | + set -e + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver --update-env-vars VERSION=$(homeassistantRelease) + displayName: 'Push details to updater' + condition: eq(variables['homeassistantReleaseStable'], 'true')) From b1a9fa47ca4b5c4738208ac4790389daf5d31bd4 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 27 Sep 2019 12:57:47 -0400 Subject: [PATCH 188/296] Add device action support for ZHA (#26903) * start implementing device actions * rename file * cleanup and add tests * fix docstrings * sort imports --- .../components/zha/.translations/en.json | 120 ++++++++-------- homeassistant/components/zha/core/helpers.py | 19 ++- homeassistant/components/zha/device_action.py | 92 ++++++++++++ .../components/zha/device_trigger.py | 19 +-- homeassistant/components/zha/strings.json | 4 + tests/components/zha/test_device_action.py | 133 ++++++++++++++++++ ...e_automation.py => test_device_trigger.py} | 2 +- 7 files changed, 313 insertions(+), 76 deletions(-) create mode 100644 homeassistant/components/zha/device_action.py create mode 100644 tests/components/zha/test_device_action.py rename tests/components/zha/{test_device_automation.py => test_device_trigger.py} (99%) diff --git a/homeassistant/components/zha/.translations/en.json b/homeassistant/components/zha/.translations/en.json index 84b335bdeaac95..ea1ad48bbff940 100644 --- a/homeassistant/components/zha/.translations/en.json +++ b/homeassistant/components/zha/.translations/en.json @@ -1,63 +1,67 @@ { - "config": { - "abort": { - "single_instance_allowed": "Only a single configuration of ZHA is allowed." - }, - "error": { - "cannot_connect": "Unable to connect to ZHA device." - }, - "step": { - "user": { - "data": { - "radio_type": "Radio Type", - "usb_path": "USB Device Path" - }, - "title": "ZHA" - } + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of ZHA is allowed." + }, + "error": { + "cannot_connect": "Unable to connect to ZHA device." + }, + "step": { + "user": { + "data": { + "radio_type": "Radio Type", + "usb_path": "USB Device Path" }, "title": "ZHA" + } }, - "device_automation": { - "trigger_subtype": { - "both_buttons": "Both buttons", - "button_1": "First button", - "button_2": "Second button", - "button_3": "Third button", - "button_4": "Fourth button", - "button_5": "Fifth button", - "button_6": "Sixth button", - "close": "Close", - "dim_down": "Dim down", - "dim_up": "Dim up", - "face_1": "with face 1 activated", - "face_2": "with face 2 activated", - "face_3": "with face 3 activated", - "face_4": "with face 4 activated", - "face_5": "with face 5 activated", - "face_6": "with face 6 activated", - "face_any": "With any/specified face(s) activated", - "left": "Left", - "open": "Open", - "right": "Right", - "turn_off": "Turn off", - "turn_on": "Turn on" - }, - "trigger_type": { - "device_dropped": "Device dropped", - "device_flipped": "Device flipped \"{subtype}\"", - "device_knocked": "Device knocked \"{subtype}\"", - "device_rotated": "Device rotated \"{subtype}\"", - "device_shaken": "Device shaken", - "device_slid": "Device slid \"{subtype}\"", - "device_tilted": "Device tilted", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_triple_press": "\"{subtype}\" button triple clicked" - } + "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Warn" + }, + "trigger_subtype": { + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "close": "Close", + "dim_down": "Dim down", + "dim_up": "Dim up", + "face_1": "with face 1 activated", + "face_2": "with face 2 activated", + "face_3": "with face 3 activated", + "face_4": "with face 4 activated", + "face_5": "with face 5 activated", + "face_6": "with face 6 activated", + "face_any": "With any/specified face(s) activated", + "left": "Left", + "open": "Open", + "right": "Right", + "turn_off": "Turn off", + "turn_on": "Turn on" + }, + "trigger_type": { + "device_dropped": "Device dropped", + "device_flipped": "Device flipped \"{subtype}\"", + "device_knocked": "Device knocked \"{subtype}\"", + "device_rotated": "Device rotated \"{subtype}\"", + "device_shaken": "Device shaken", + "device_slid": "Device slid \"{subtype}\"", + "device_tilted": "Device tilted", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_triple_press": "\"{subtype}\" button triple clicked" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 37bc6c7a2c17d0..b07658e72d01eb 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -10,7 +10,14 @@ from homeassistant.core import callback -from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DEFAULT_BAUDRATE, RadioType +from .const import ( + CLUSTER_TYPE_IN, + CLUSTER_TYPE_OUT, + DATA_ZHA, + DATA_ZHA_GATEWAY, + DEFAULT_BAUDRATE, + RadioType, +) from .registries import BINDABLE_CLUSTERS _LOGGER = logging.getLogger(__name__) @@ -132,6 +139,16 @@ def async_is_bindable_target(source_zha_device, target_zha_device): return False +async def async_get_zha_device(hass, device_id): + """Get a ZHA device for the given device registry id.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + registry_device = device_registry.async_get(device_id) + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee_address = list(list(registry_device.identifiers)[0])[1] + ieee = convert_ieee(ieee_address) + return zha_gateway.devices[ieee] + + class LogMixin: """Log helper.""" diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py new file mode 100644 index 00000000000000..27e78507bfb317 --- /dev/null +++ b/homeassistant/components/zha/device_action.py @@ -0,0 +1,92 @@ +"""Provides device actions for ZHA devices.""" +from typing import List + +import voluptuous as vol + +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import config_validation as cv, service +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from . import DOMAIN +from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN +from .core.const import CHANNEL_IAS_WD +from .core.helpers import async_get_zha_device + +ACTION_SQUAWK = "squawk" +ACTION_WARN = "warn" +ATTR_DATA = "data" +ATTR_IEEE = "ieee" +CONF_ZHA_ACTION_TYPE = "zha_action_type" +ZHA_ACTION_TYPE_SERVICE_CALL = "service_call" + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN, vol.Required(CONF_TYPE): str} +) + +DEVICE_ACTIONS = { + CHANNEL_IAS_WD: [ + {CONF_TYPE: ACTION_SQUAWK, CONF_DOMAIN: DOMAIN}, + {CONF_TYPE: ACTION_WARN, CONF_DOMAIN: DOMAIN}, + ] +} + +DEVICE_ACTION_TYPES = { + ACTION_SQUAWK: ZHA_ACTION_TYPE_SERVICE_CALL, + ACTION_WARN: ZHA_ACTION_TYPE_SERVICE_CALL, +} + +SERVICE_NAMES = { + ACTION_SQUAWK: SERVICE_WARNING_DEVICE_SQUAWK, + ACTION_WARN: SERVICE_WARNING_DEVICE_WARN, +} + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, +) -> None: + """Perform an action based on configuration.""" + config = ACTION_SCHEMA(config) + await ZHA_ACTION_TYPES[DEVICE_ACTION_TYPES[config[CONF_TYPE]]]( + hass, config, variables, context + ) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions.""" + zha_device = await async_get_zha_device(hass, device_id) + actions = [ + action + for channel in DEVICE_ACTIONS + for action in DEVICE_ACTIONS[channel] + if channel in zha_device.cluster_channels + ] + for action in actions: + action[CONF_DEVICE_ID] = device_id + return actions + + +async def _execute_service_based_action( + hass: HomeAssistant, + config: ACTION_SCHEMA, + variables: TemplateVarsType, + context: Context, +) -> None: + action_type = config[CONF_TYPE] + service_name = SERVICE_NAMES[action_type] + zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) + + service_action = { + service.CONF_SERVICE: "{}.{}".format(DOMAIN, service_name), + ATTR_DATA: {ATTR_IEEE: str(zha_device.ieee)}, + } + + await service.async_call_from_config( + hass, service_action, blocking=True, variables=variables, context=context + ) + + +ZHA_ACTION_TYPES = {ZHA_ACTION_TYPE_SERVICE_CALL: _execute_service_based_action} diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 46e3beafcaedfd..37ec14bc433a2b 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -9,8 +9,7 @@ from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from . import DOMAIN -from .core.const import DATA_ZHA, DATA_ZHA_GATEWAY -from .core.helpers import convert_ieee +from .core.helpers import async_get_zha_device CONF_SUBTYPE = "subtype" DEVICE = "device" @@ -26,7 +25,7 @@ async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" config = TRIGGER_SCHEMA(config) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - zha_device = await _async_get_zha_device(hass, config[CONF_DEVICE_ID]) + zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) if ( zha_device.device_automation_triggers is None @@ -52,7 +51,7 @@ async def async_get_triggers(hass, device_id): Make sure the device supports device automations and if it does return the trigger list. """ - zha_device = await _async_get_zha_device(hass, device_id) + zha_device = await async_get_zha_device(hass, device_id) if not zha_device.device_automation_triggers: return @@ -70,15 +69,3 @@ async def async_get_triggers(hass, device_id): ) return triggers - - -async def _async_get_zha_device(hass, device_id): - device_registry = await hass.helpers.device_registry.async_get_registry() - registry_device = device_registry.async_get(device_id) - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee_address = list(list(registry_device.identifiers)[0])[1] - ieee = convert_ieee(ieee_address) - zha_device = zha_gateway.devices[ieee] - if not zha_device: - raise InvalidDeviceAutomationConfig - return zha_device diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index cfc32a020c6dd5..a41f6de24be2b4 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -18,6 +18,10 @@ } }, "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Warn" + }, "trigger_type": { "remote_button_short_press": "\"{subtype}\" button pressed", "remote_button_short_release": "\"{subtype}\" button released", diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py new file mode 100644 index 00000000000000..6e7bc6ab4b13a7 --- /dev/null +++ b/tests/components/zha/test_device_action.py @@ -0,0 +1,133 @@ +"""The test for zha device automation actions.""" +from unittest.mock import patch + +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.components.zha import DOMAIN +from homeassistant.components.zha.core.const import CHANNEL_ON_OFF +from homeassistant.helpers.device_registry import async_get_registry +from homeassistant.setup import async_setup_component + +from .common import async_enable_traffic, async_init_zigpy_device + +from tests.common import async_mock_service, mock_coro + +SHORT_PRESS = "remote_button_short_press" +COMMAND = "command" +COMMAND_SINGLE = "single" + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "zha", "warning_device_warn") + + +async def test_get_actions(hass, config_entry, zha_gateway): + """Test we get the expected actions from a zha device.""" + from zigpy.zcl.clusters.general import Basic + from zigpy.zcl.clusters.security import IasZone, IasWd + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, + [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id], + [], + None, + zha_gateway, + ) + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + ieee_address = str(zha_device.ieee) + + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set()) + + actions = await async_get_device_automations(hass, "action", reg_device.id) + + expected_actions = [ + {"domain": DOMAIN, "type": "squawk", "device_id": reg_device.id}, + {"domain": DOMAIN, "type": "warn", "device_id": reg_device.id}, + ] + + assert actions == expected_actions + + +async def test_action(hass, config_entry, zha_gateway, calls): + """Test for executing a zha device action.""" + + from zigpy.zcl.clusters.general import Basic, OnOff + from zigpy.zcl.clusters.security import IasZone, IasWd + from zigpy.zcl.foundation import Status + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, + [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id], + [OnOff.cluster_id], + None, + zha_gateway, + ) + + zigpy_device.device_automation_triggers = { + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE} + } + + await hass.config_entries.async_forward_entry_setup(config_entry, "switch") + await hass.async_block_till_done() + + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + ieee_address = str(zha_device.ieee) + + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set()) + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS]) + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": SHORT_PRESS, + "subtype": SHORT_PRESS, + }, + "action": { + "domain": DOMAIN, + "device_id": reg_device.id, + "type": "warn", + }, + } + ] + }, + ) + + await hass.async_block_till_done() + + on_off_channel = zha_device.cluster_channels[CHANNEL_ON_OFF] + on_off_channel.zha_send_event(on_off_channel.cluster, COMMAND_SINGLE, []) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == DOMAIN + assert calls[0].service == "warning_device_warn" + assert calls[0].data["ieee"] == ieee_address diff --git a/tests/components/zha/test_device_automation.py b/tests/components/zha/test_device_trigger.py similarity index 99% rename from tests/components/zha/test_device_automation.py rename to tests/components/zha/test_device_trigger.py index 5a4b9d5616e8fe..2f4ddb6b8b2cf3 100644 --- a/tests/components/zha/test_device_automation.py +++ b/tests/components/zha/test_device_trigger.py @@ -1,4 +1,4 @@ -"""ZHA device automation tests.""" +"""ZHA device automation trigger tests.""" from unittest.mock import patch import pytest From eeffd090a31d7612977e6076b98523028fc5bc01 Mon Sep 17 00:00:00 2001 From: Andrew Onyshchuk Date: Fri, 27 Sep 2019 12:21:04 -0500 Subject: [PATCH 189/296] Add support for Z-Wave battery level (#26943) * Add support for Z-Wave battery level * Improve coverage --- .../components/zwave/discovery_schemas.py | 1 + homeassistant/components/zwave/sensor.py | 13 ++++++++++++- tests/components/zwave/test_sensor.py | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index dbec1484508cf9..e2254073290e86 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -277,6 +277,7 @@ const.COMMAND_CLASS_ALARM, const.COMMAND_CLASS_SENSOR_ALARM, const.COMMAND_CLASS_INDICATOR, + const.COMMAND_CLASS_BATTERY, ], const.DISC_GENRE: const.GENRE_USER, } diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py index 240809548ee658..0820feb8d0f4d8 100644 --- a/homeassistant/components/zwave/sensor.py +++ b/homeassistant/components/zwave/sensor.py @@ -1,7 +1,7 @@ """Support for Z-Wave sensors.""" import logging from homeassistant.core import callback -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import DOMAIN, DEVICE_CLASS_BATTERY from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import const, ZWaveDeviceEntity @@ -28,6 +28,8 @@ def async_add_sensor(sensor): def get_device(node, values, **kwargs): """Create Z-Wave entity device.""" # Generic Device mappings + if values.primary.command_class == const.COMMAND_CLASS_BATTERY: + return ZWaveBatterySensor(values) if node.has_command_class(const.COMMAND_CLASS_SENSOR_MULTILEVEL): return ZWaveMultilevelSensor(values) if ( @@ -107,3 +109,12 @@ class ZWaveAlarmSensor(ZWaveSensor): """ pass + + +class ZWaveBatterySensor(ZWaveSensor): + """Representation of Z-Wave device battery level.""" + + @property + def device_class(self): + """Return the class of this device.""" + return DEVICE_CLASS_BATTERY diff --git a/tests/components/zwave/test_sensor.py b/tests/components/zwave/test_sensor.py index f18a66d9a5b676..cec93f5af4ac64 100644 --- a/tests/components/zwave/test_sensor.py +++ b/tests/components/zwave/test_sensor.py @@ -53,6 +53,23 @@ def test_get_device_detects_multilevel_meter(mock_openzwave): assert isinstance(device, sensor.ZWaveMultilevelSensor) +def test_get_device_detects_battery_sensor(mock_openzwave): + """Test get_device returns a Z-Wave battery sensor.""" + + node = MockNode(command_classes=[const.COMMAND_CLASS_BATTERY]) + value = MockValue( + data=0, + node=node, + type=const.TYPE_DECIMAL, + command_class=const.COMMAND_CLASS_BATTERY, + ) + values = MockEntityValues(primary=value) + + device = sensor.get_device(node=node, values=values, node_config={}) + assert isinstance(device, sensor.ZWaveBatterySensor) + assert device.device_class == homeassistant.const.DEVICE_CLASS_BATTERY + + def test_multilevelsensor_value_changed_temp_fahrenheit(mock_openzwave): """Test value changed for Z-Wave multilevel sensor for temperature.""" node = MockNode( From 80bc15e24b5c5665aa26ece36630dc6d6badc9de Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 27 Sep 2019 21:51:46 +0200 Subject: [PATCH 190/296] Update Alexa discovery description (#26933) * Update Alexa discovery description * Update description * Fix test * Filter special chars --- homeassistant/components/alexa/entities.py | 20 +++++++++++++------- tests/components/alexa/test_smart_home.py | 18 +++++++++++++----- tests/components/cloud/test_client.py | 2 +- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 03d153f5927c31..f0d72af23d50f4 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -52,6 +52,8 @@ ENTITY_ADAPTERS = Registry() +TRANSLATION_TABLE = dict.fromkeys(map(ord, r"}{\/|\"()[]+~!><*%"), None) + class DisplayCategory: """Possible display categories for Discovery response. @@ -134,15 +136,18 @@ def entity_id(self): def friendly_name(self): """Return the Alexa API friendly name.""" - return self.entity_conf.get(CONF_NAME, self.entity.name) + return self.entity_conf.get(CONF_NAME, self.entity.name).translate( + TRANSLATION_TABLE + ) def description(self): """Return the Alexa API description.""" - return self.entity_conf.get(CONF_DESCRIPTION, self.entity.entity_id) + description = self.entity_conf.get(CONF_DESCRIPTION) or self.entity_id + return f"{description} via Home Assistant".translate(TRANSLATION_TABLE) def alexa_id(self): """Return the Alexa API entity id.""" - return self.entity.entity_id.replace(".", "#") + return self.entity.entity_id.replace(".", "#").translate(TRANSLATION_TABLE) def display_categories(self): """Return a list of display categories.""" @@ -389,10 +394,11 @@ class SceneCapabilities(AlexaEntity): """Class to represent Scene capabilities.""" def description(self): - """Return the description of the entity.""" - # Required description as per Amazon Scene docs - scene_fmt = "{} (Scene connected via Home Assistant)" - return scene_fmt.format(AlexaEntity.description(self)) + """Return the Alexa API description.""" + description = AlexaEntity.description(self) + if "scene" not in description.casefold(): + return f"{description} (Scene)" + return description def default_display_categories(self): """Return the display categories for this entity.""" diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index bd5a4d25edd10c..3cafa89902471f 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1162,14 +1162,16 @@ async def test_entity_config(hass): request = get_new_request("Alexa.Discovery", "Discover") hass.states.async_set("light.test_1", "on", {"friendly_name": "Test light 1"}) + hass.states.async_set("scene.test_1", "scening", {"friendly_name": "Test 1"}) alexa_config = MockConfig(hass) alexa_config.entity_config = { "light.test_1": { - "name": "Config name", + "name": "Config *name*", "display_categories": "SWITCH", - "description": "Config description", - } + "description": "Config >! Date: Fri, 27 Sep 2019 12:54:17 -0700 Subject: [PATCH 191/296] Add templates to scaffold device_trigger, device_condition, (#26871) device_action --- .../components/zha/device_trigger.py | 4 +- script/scaffold/__main__.py | 4 +- script/scaffold/docs.py | 27 ++++ .../integration/device_action.py | 84 +++++++++++ .../device_action/tests/test_device_action.py | 103 ++++++++++++++ .../integration/device_condition.py | 64 +++++++++ .../tests/test_device_condition.py | 125 +++++++++++++++++ .../integration/device_trigger.py | 100 +++++++++++++ .../tests/test_device_trigger.py | 131 ++++++++++++++++++ 9 files changed, 638 insertions(+), 4 deletions(-) create mode 100644 script/scaffold/templates/device_action/integration/device_action.py create mode 100644 script/scaffold/templates/device_action/tests/test_device_action.py create mode 100644 script/scaffold/templates/device_condition/integration/device_condition.py create mode 100644 script/scaffold/templates/device_condition/tests/test_device_condition.py create mode 100644 script/scaffold/templates/device_trigger/integration/device_trigger.py create mode 100644 script/scaffold/templates/device_trigger/tests/test_device_trigger.py diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 37ec14bc433a2b..331dc3d32968d3 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -35,13 +35,13 @@ async def async_attach_trigger(hass, config, action, automation_info): trigger = zha_device.device_automation_triggers[trigger] - state_config = { + event_config = { event.CONF_EVENT_TYPE: ZHA_EVENT, event.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger}, } return await event.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, event_config, action, automation_info, platform_type="device" ) diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 22cdee8f69e0e5..2258840f430d92 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -65,10 +65,10 @@ def main(): print() print("Running tests") - print(f"$ pytest -v tests/components/{info.domain}") + print(f"$ pytest -vvv tests/components/{info.domain}") if ( subprocess.run( - f"pytest -v tests/components/{info.domain}", shell=True + f"pytest -vvv tests/components/{info.domain}", shell=True ).returncode != 0 ): diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 801b8ebb5fd8b0..47cb9709c3d405 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -29,5 +29,32 @@ def print_relevant_docs(template: str, info: Info) -> None: - {info.tests_dir / "test_reproduce_state.py"} Please update the relevant items marked as TODO before submitting a pull request. +""" + ) + + elif template == "device_trigger": + print( + f""" +Device trigger base has been added to the {info.domain} integration: + - {info.integration_dir / "device_trigger.py"} + - {info.tests_dir / "test_device_trigger.py"} +""" + ) + + elif template == "device_condition": + print( + f""" +Device condition base has been added to the {info.domain} integration: + - {info.integration_dir / "device_condition.py"} + - {info.tests_dir / "test_device_condition.py"} +""" + ) + + elif template == "device_action": + print( + f""" +Device action base has been added to the {info.domain} integration: + - {info.integration_dir / "device_action.py"} + - {info.tests_dir / "test_device_action.py"} """ ) diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py new file mode 100644 index 00000000000000..d5674f01b2de4a --- /dev/null +++ b/script/scaffold/templates/device_action/integration/device_action.py @@ -0,0 +1,84 @@ +"""Provides device automations for NEW_NAME.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + SERVICE_TURN_ON, + SERVICE_TURN_OFF, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import DOMAIN + +# TODO specify your supported action types. +ACTION_TYPES = {"turn_on", "turn_off"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for NEW_NAME devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # TODO Read this comment and remove it. + # This example shows how to iterate over the entities of this device + # that match this integration. If your actions instead rely on + # calling services, do something like: + # zha_device = await _async_get_zha_device(hass, device_id) + # return zha_device.device_actions + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add actions for each entity that belongs to this integration + # TODO add your own actions. + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_on", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_off", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "turn_on": + service = SERVICE_TURN_ON + elif config[CONF_TYPE] == "turn_off": + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) diff --git a/script/scaffold/templates/device_action/tests/test_device_action.py b/script/scaffold/templates/device_action/tests/test_device_action.py new file mode 100644 index 00000000000000..f8a00bf1ec8232 --- /dev/null +++ b/script/scaffold/templates/device_action/tests/test_device_action.py @@ -0,0 +1,103 @@ +"""The tests for NEW_NAME device actions.""" +import pytest + +from homeassistant.components.NEW_DOMAIN import DOMAIN +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": "NEW_DOMAIN.test_5678", + }, + { + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": "NEW_DOMAIN.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert actions == expected_actions + + +async def test_action(hass): + """Test for turn_on and turn_off actions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_turn_off", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "NEW_DOMAIN.entity", + "type": "turn_off", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_turn_on", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "NEW_DOMAIN.entity", + "type": "turn_on", + }, + }, + ] + }, + ) + + turn_off_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_off") + turn_on_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_on") + + hass.bus.async_fire("test_event_turn_off") + await hass.async_block_till_done() + assert len(turn_off_calls) == 1 + assert len(turn_on_calls) == 0 + + hass.bus.async_fire("test_event_turn_on") + await hass.async_block_till_done() + assert len(turn_off_calls) == 1 + assert len(turn_on_calls) == 1 diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py new file mode 100644 index 00000000000000..d19fa8817a0299 --- /dev/null +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -0,0 +1,64 @@ +"""Provides device automations for NEW_NAME.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +# TODO specify your supported condition types. +CONDITION_TYPES = {"is_on"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES)} +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[str]: + """List device conditions for NEW_NAME devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add conditions for each entity that belongs to this integration + # TODO add your own conditions. + conditions.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_on", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + + def test_is_on(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is on.""" + return condition.state(hass, config[ATTR_ENTITY_ID], STATE_ON) + + return test_is_on diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py new file mode 100644 index 00000000000000..d9cef083510b0f --- /dev/null +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -0,0 +1,125 @@ +"""The tests for NEW_NAME device conditions.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("NEW_DOMAIN.entity", STATE_ON) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "NEW_DOMAIN.entity", + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "NEW_DOMAIN.entity", + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on - event - test_event1" + + hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off - event - test_event2" diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py new file mode 100644 index 00000000000000..f7e9fc091f8809 --- /dev/null +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -0,0 +1,100 @@ +"""Provides device automations for NEW_NAME.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_ON, + STATE_OFF, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import state, AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import DOMAIN + +# TODO specify your supported trigger types. +TRIGGER_TYPES = {"turned_on", "turned_off"} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES)} +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for NEW_NAME devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # TODO Read this comment and remove it. + # This example shows how to iterate over the entities of this device + # that match this integration. If your triggers instead rely on + # events fired by devices without entities, do something like: + # zha_device = await _async_get_zha_device(hass, device_id) + # return zha_device.device_triggers + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add triggers for each entity that belongs to this integration + # TODO add your own triggers. + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turned_on", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turned_off", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + # TODO Implement your own logic to attach triggers. + # Generally we suggest to re-use the existing state or event + # triggers from the automation integration. + + if config[CONF_TYPE] == "turned_on": + from_state = STATE_OFF + to_state = STATE_ON + else: + from_state = STATE_ON + to_state = STATE_OFF + + return state.async_attach_trigger( + hass, + { + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + }, + action, + automation_info, + platform_type="device", + ) diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py new file mode 100644 index 00000000000000..c22197bb136832 --- /dev/null +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -0,0 +1,131 @@ +"""The tests for NEW_NAME device triggers.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "NEW_DOMAIN.entity", + "type": "turned_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "turn_on - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "NEW_DOMAIN.entity", + "type": "turned_off", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "turn_off - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is turning on. + hass.states.async_set("NEW_DOMAIN.entity", STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "turn_on - device - {} - off - on - None".format( + "NEW_DOMAIN.entity" + ) + + # Fake that the entity is turning off. + hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_off - device - {} - on - off - None".format( + "NEW_DOMAIN.entity" + ) From fde128d66ce7634c7d776b7fba2dba71f8e26122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 27 Sep 2019 22:57:59 +0300 Subject: [PATCH 192/296] Upgrade mypy to 0.730, address raised issues (#26959) https://mypy-lang.blogspot.com/2019/09/mypy-730-released.html --- homeassistant/auth/mfa_modules/notify.py | 6 ++++-- homeassistant/auth/mfa_modules/totp.py | 5 +++-- homeassistant/components/http/static.py | 8 +++++--- homeassistant/core.py | 4 ++-- homeassistant/helpers/config_validation.py | 3 +-- homeassistant/helpers/deprecation.py | 2 +- homeassistant/util/async_.py | 2 +- homeassistant/util/location.py | 4 ++-- homeassistant/util/logging.py | 6 ++++-- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 11 files changed, 25 insertions(+), 19 deletions(-) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 01c5c12efb7588..b14f5fedc22f55 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -251,8 +251,10 @@ async def async_notify_user(self, user_id: str, code: str) -> None: _LOGGER.error("Cannot find user %s", user_id) return - await self.async_notify( # type: ignore - code, notify_setting.notify_service, notify_setting.target + await self.async_notify( + code, + notify_setting.notify_service, # type: ignore + notify_setting.target, ) async def async_notify( diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 4e417fca2198b1..9829044a53ece5 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -215,8 +215,9 @@ async def async_step_init( else: hass = self._auth_module.hass - self._ota_secret, self._url, self._image = await hass.async_add_executor_job( # type: ignore - _generate_secret_and_qr_code, str(self._user.name) + self._ota_secret, self._url, self._image = await hass.async_add_executor_job( + _generate_secret_and_qr_code, # type: ignore + str(self._user.name), ) return self.async_show_form( diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 952ca473fdc5f8..e6a70c9f64369a 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -42,8 +42,10 @@ async def _handle(self, request): if filepath.is_dir(): return await super()._handle(request) if filepath.is_file(): - # type ignore: https://github.com/aio-libs/aiohttp/pull/3976 - return FileResponse( # type: ignore - filepath, chunk_size=self._chunk_size, headers=CACHE_HEADERS + return FileResponse( + filepath, + chunk_size=self._chunk_size, + # type ignore: https://github.com/aio-libs/aiohttp/pull/3976 + headers=CACHE_HEADERS, # type: ignore ) raise HTTPNotFound diff --git a/homeassistant/core.py b/homeassistant/core.py index 31761f2560faf4..e011db33c343a8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -144,8 +144,8 @@ def async_loop_exception_handler(_: Any, context: Dict) -> None: if exception: kwargs["exc_info"] = (type(exception), exception, exception.__traceback__) - _LOGGER.error( # type: ignore - "Error doing job: %s", context["message"], **kwargs + _LOGGER.error( + "Error doing job: %s", context["message"], **kwargs # type: ignore ) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index d567962e328a8e..d0aeb4f4968bc6 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -598,8 +598,7 @@ def deprecated( else: # Unclear when it is None, but it happens, so let's guard. # https://github.com/home-assistant/home-assistant/issues/24982 - # type ignore/unreachable: https://github.com/python/typeshed/pull/3137 - module_name = __name__ # type: ignore + module_name = __name__ if replacement_key and invalidation_version: warning = ( diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index db1c34d8fd4529..881534b5bed70c 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -54,7 +54,7 @@ def get_deprecated( and a warning is issued to the user. """ if old_name in config: - module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ + module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ # type: ignore logger = logging.getLogger(module_name) logger.warning( "'%s' is deprecated. Please rename '%s' to '%s' in your " diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 271b9caa62ae59..d43658d1584269 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -24,7 +24,7 @@ try: # pylint: disable=invalid-name - asyncio_run = asyncio.run + asyncio_run = asyncio.run # type: ignore except AttributeError: _T = TypeVar("_T") diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 7c61a8ab1e9197..f81c40a52bb675 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -6,7 +6,7 @@ import asyncio import collections import math -from typing import Any, Optional, Tuple, Dict +from typing import Any, Optional, Tuple, Dict, cast import aiohttp @@ -159,7 +159,7 @@ def vincenty( if miles: s *= MILES_PER_KILOMETER # kilometers to miles - return round(s, 6) + return round(cast(float, s), 6) async def _get_ipapi(session: aiohttp.ClientSession) -> Optional[Dict[str, Any]]: diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 236e2fc1aa24de..79cb2607b1045f 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -130,7 +130,7 @@ def catch_log_exception( """Decorate a callback to catch and log exceptions.""" def log_exception(*args: Any) -> None: - module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ + module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ # type: ignore # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) @@ -178,7 +178,9 @@ async def coro_wrapper(*args: Any) -> Any: try: return await target except Exception: # pylint: disable=broad-except - module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ + module_name = inspect.getmodule( # type: ignore + inspect.trace()[1][0] + ).__name__ # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) diff --git a/requirements_test.txt b/requirements_test.txt index ae4401178b1809..7e5be09a28ba9c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ codecov==2.0.15 flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 -mypy==0.720 +mypy==0.730 pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3f40e16349292..a860c67dbd1368 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ codecov==2.0.15 flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 -mypy==0.720 +mypy==0.730 pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 From 58446c79fc31216898c50dd1bfa0ef3709c9a75e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Sep 2019 13:08:30 -0700 Subject: [PATCH 193/296] Update scaffold text --- script/scaffold/docs.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 47cb9709c3d405..ab87799d6b2ff4 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -28,7 +28,8 @@ def print_relevant_docs(template: str, info: Info) -> None: - {info.integration_dir / "reproduce_state.py"} - {info.tests_dir / "test_reproduce_state.py"} -Please update the relevant items marked as TODO before submitting a pull request. +You will now need to update the code to make sure that every attribute +that can occur in the state will cause the right service to be called. """ ) @@ -38,6 +39,9 @@ def print_relevant_docs(template: str, info: Info) -> None: Device trigger base has been added to the {info.domain} integration: - {info.integration_dir / "device_trigger.py"} - {info.tests_dir / "test_device_trigger.py"} + +You will now need to update the code to make sure that relevant triggers +are exposed. """ ) @@ -47,6 +51,9 @@ def print_relevant_docs(template: str, info: Info) -> None: Device condition base has been added to the {info.domain} integration: - {info.integration_dir / "device_condition.py"} - {info.tests_dir / "test_device_condition.py"} + +You will now need to update the code to make sure that relevant condtions +are exposed. """ ) @@ -56,5 +63,8 @@ def print_relevant_docs(template: str, info: Info) -> None: Device action base has been added to the {info.domain} integration: - {info.integration_dir / "device_action.py"} - {info.tests_dir / "test_device_action.py"} + +You will now need to update the code to make sure that relevant services +are exposed as actions. """ ) From fc3f5163f13e202889260db477e7173a6cfaa34c Mon Sep 17 00:00:00 2001 From: Khole Date: Fri, 27 Sep 2019 22:18:34 +0100 Subject: [PATCH 194/296] Add hive boost to climate and water_heater (#26789) * Start the Boost work * Add services.yaml * Added Services #2 * Start the Boost work * Add services.yaml * Added Services #2 * Working Services * pyhiveapi to 0.2.19 * Update Libary to 0.2.19 * Update Water_heater boost * Added Async hass add function * Update Services * Reviewed Changes * Fixed Refresh System * Review 2 * Moved device iteration to the platform * update * Updates #2 * Review#3 New Base Class * Review #5 * Update homeassistant/components/hive/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/hive/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/hive/__init__.py Co-Authored-By: Martin Hjelmare * Review 6 * Review 7 * Removed Child classes to inhertit from the parent --- homeassistant/components/hive/__init__.py | 134 ++++++++++++++++-- .../components/hive/binary_sensor.py | 28 +--- homeassistant/components/hive/climate.py | 59 +++----- homeassistant/components/hive/light.py | 36 ++--- homeassistant/components/hive/manifest.json | 2 +- homeassistant/components/hive/sensor.py | 35 ++--- homeassistant/components/hive/services.yaml | 27 ++++ homeassistant/components/hive/switch.py | 33 ++--- homeassistant/components/hive/water_heater.py | 49 ++----- requirements_all.txt | 2 +- 10 files changed, 218 insertions(+), 187 deletions(-) create mode 100644 homeassistant/components/hive/services.yaml diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index fc96f2d8c966f7..c11eb18accad98 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -1,17 +1,32 @@ -"""Support for the Hive devices.""" +"""Support for the Hive devices and services.""" +from functools import wraps import logging from pyhiveapi import Pyhiveapi import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) DOMAIN = "hive" DATA_HIVE = "data_hive" +SERVICES = ["Heating", "HotWater"] +SERVICE_BOOST_HOTWATER = "boost_hotwater" +SERVICE_BOOST_HEATING = "boost_heating" +ATTR_TIME_PERIOD = "time_period" +ATTR_MODE = "on_off" DEVICETYPES = { "binary_sensor": "device_list_binary_sensor", "climate": "device_list_climate", @@ -34,11 +49,31 @@ extra=vol.ALLOW_EXTRA, ) +BOOST_HEATING_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_TIME_PERIOD): vol.All( + cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() // 60 + ), + vol.Optional(ATTR_TEMPERATURE, default="25.0"): vol.Coerce(float), + } +) + +BOOST_HOTWATER_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Optional(ATTR_TIME_PERIOD, default="00:30:00"): vol.All( + cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() // 60 + ), + vol.Required(ATTR_MODE): cv.string, + } +) + class HiveSession: """Initiate Hive Session Class.""" - entities = [] + entity_lookup = {} core = None heating = None hotwater = None @@ -51,6 +86,35 @@ class HiveSession: def setup(hass, config): """Set up the Hive Component.""" + + def heating_boost(service): + """Handle the service call.""" + node_id = HiveSession.entity_lookup.get(service.data[ATTR_ENTITY_ID]) + if not node_id: + # log or raise error + _LOGGER.error("Cannot boost entity id entered") + return + + minutes = service.data[ATTR_TIME_PERIOD] + temperature = service.data[ATTR_TEMPERATURE] + + session.heating.turn_boost_on(node_id, minutes, temperature) + + def hotwater_boost(service): + """Handle the service call.""" + node_id = HiveSession.entity_lookup.get(service.data[ATTR_ENTITY_ID]) + if not node_id: + # log or raise error + _LOGGER.error("Cannot boost entity id entered") + return + minutes = service.data[ATTR_TIME_PERIOD] + mode = service.data[ATTR_MODE] + + if mode == "on": + session.hotwater.turn_boost_on(node_id, minutes) + elif mode == "off": + session.hotwater.turn_boost_off(node_id) + session = HiveSession() session.core = Pyhiveapi() @@ -58,9 +122,9 @@ def setup(hass, config): password = config[DOMAIN][CONF_PASSWORD] update_interval = config[DOMAIN][CONF_SCAN_INTERVAL] - devicelist = session.core.initialise_api(username, password, update_interval) + devices = session.core.initialise_api(username, password, update_interval) - if devicelist is None: + if devices is None: _LOGGER.error("Hive API initialization failed") return False @@ -73,9 +137,59 @@ def setup(hass, config): session.attributes = Pyhiveapi.Attributes() hass.data[DATA_HIVE] = session - for ha_type, hive_type in DEVICETYPES.items(): - for key, devices in devicelist.items(): - if key == hive_type: - for hivedevice in devices: - load_platform(hass, ha_type, DOMAIN, hivedevice, config) + for ha_type in DEVICETYPES: + devicelist = devices.get(DEVICETYPES[ha_type]) + if devicelist: + load_platform(hass, ha_type, DOMAIN, devicelist, config) + if ha_type == "climate": + hass.services.register( + DOMAIN, + SERVICE_BOOST_HEATING, + heating_boost, + schema=BOOST_HEATING_SCHEMA, + ) + if ha_type == "water_heater": + hass.services.register( + DOMAIN, + SERVICE_BOOST_HEATING, + hotwater_boost, + schema=BOOST_HOTWATER_SCHEMA, + ) + return True + + +def refresh_system(func): + """Force update all entities after state change.""" + + @wraps(func) + def wrapper(self, *args, **kwargs): + func(self, *args, **kwargs) + dispatcher_send(self.hass, DOMAIN) + + return wrapper + + +class HiveEntity(Entity): + """Initiate Hive Base Class.""" + + def __init__(self, session, hive_device): + """Initialize the instance.""" + self.node_id = hive_device["Hive_NodeID"] + self.node_name = hive_device["Hive_NodeName"] + self.device_type = hive_device["HA_DeviceType"] + self.node_device_type = hive_device["Hive_DeviceType"] + self.session = session + self.attributes = {} + self._unique_id = f"{self.node_id}-{self.device_type}" + + async def async_added_to_hass(self): + """When entity is added to Home Assistant.""" + async_dispatcher_connect(self.hass, DOMAIN, self._update_callback) + if self.device_type in SERVICES: + self.session.entity_lookup[self.entity_id] = self.node_id + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 50c8277302fbc7..ce7e53b77a5263 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,7 +1,7 @@ """Support for the Hive binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice -from . import DATA_HIVE, DOMAIN +from . import DOMAIN, DATA_HIVE, HiveEntity DEVICETYPE_DEVICE_CLASS = {"motionsensor": "motion", "contactsensor": "opening"} @@ -10,26 +10,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive sensor devices.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - add_entities([HiveBinarySensorEntity(session, discovery_info)]) + session = hass.data.get(DATA_HIVE) + devs = [] + for dev in discovery_info: + devs.append(HiveBinarySensorEntity(session, dev)) + add_entities(devs) -class HiveBinarySensorEntity(BinarySensorDevice): +class HiveBinarySensorEntity(HiveEntity, BinarySensorDevice): """Representation of a Hive binary sensor.""" - def __init__(self, hivesession, hivedevice): - """Initialize the hive sensor.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.node_device_type = hivedevice["Hive_DeviceType"] - self.session = hivesession - self.attributes = {} - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self.session.entities.append(self) - @property def unique_id(self): """Return unique ID of entity.""" @@ -40,11 +31,6 @@ def device_info(self): """Return device information.""" return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def device_class(self): """Return the class of this sensor.""" diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 861957e6ef0166..1fb77ce6cb9731 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -5,13 +5,14 @@ HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_BOOST, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from . import DATA_HIVE, DOMAIN + +from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system HIVE_TO_HASS_STATE = { "SCHEDULE": HVAC_MODE_AUTO, @@ -34,28 +35,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive climate devices.""" if discovery_info is None: return - if discovery_info["HA_DeviceType"] != "Heating": - return session = hass.data.get(DATA_HIVE) - climate = HiveClimateEntity(session, discovery_info) - - add_entities([climate]) + devs = [] + for dev in discovery_info: + devs.append(HiveClimateEntity(session, dev)) + add_entities(devs) -class HiveClimateEntity(ClimateDevice): +class HiveClimateEntity(HiveEntity, ClimateDevice): """Hive Climate Device.""" - def __init__(self, hivesession, hivedevice): + def __init__(self, hive_session, hive_device): """Initialize the Climate device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.thermostat_node_id = hivedevice["Thermostat_NodeID"] - self.session = hivesession - self.attributes = {} - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" + super().__init__(hive_session, hive_device) + self.thermostat_node_id = hive_device["Thermostat_NodeID"] @property def unique_id(self): @@ -72,11 +66,6 @@ def supported_features(self): """Return the list of supported features.""" return SUPPORT_FLAGS - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the name of the Climate device.""" @@ -99,7 +88,7 @@ def hvac_modes(self): return SUPPORT_HVAC @property - def hvac_mode(self) -> str: + def hvac_mode(self): """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. @@ -143,43 +132,29 @@ def preset_modes(self): """Return a list of available preset modes.""" return SUPPORT_PRESET - async def async_added_to_hass(self): - """When entity is added to Home Assistant.""" - await super().async_added_to_hass() - self.session.entities.append(self) - + @refresh_system def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" new_mode = HASS_TO_HIVE_STATE[hvac_mode] self.session.heating.set_mode(self.node_id, new_mode) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - + @refresh_system def set_temperature(self, **kwargs): """Set new target temperature.""" new_temperature = kwargs.get(ATTR_TEMPERATURE) if new_temperature is not None: self.session.heating.set_target_temperature(self.node_id, new_temperature) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - - def set_preset_mode(self, preset_mode) -> None: + @refresh_system + def set_preset_mode(self, preset_mode): """Set new preset mode.""" if preset_mode == PRESET_NONE and self.preset_mode == PRESET_BOOST: self.session.heating.turn_boost_off(self.node_id) - elif preset_mode == PRESET_BOOST: - curtemp = self.session.heating.current_temperature(self.node_id) - curtemp = round(curtemp * 2) / 2 + curtemp = round(self.current_temperature * 2) / 2 temperature = curtemp + 0.5 - self.session.heating.turn_boost_on(self.node_id, 30, temperature) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - def update(self): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index a85c3a43992eb8..41fc286d13b0e7 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -10,32 +10,28 @@ ) import homeassistant.util.color as color_util -from . import DATA_HIVE, DOMAIN +from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive light devices.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - add_entities([HiveDeviceLight(session, discovery_info)]) + session = hass.data.get(DATA_HIVE) + devs = [] + for dev in discovery_info: + devs.append(HiveDeviceLight(session, dev)) + add_entities(devs) -class HiveDeviceLight(Light): +class HiveDeviceLight(HiveEntity, Light): """Hive Active Light Device.""" - def __init__(self, hivesession, hivedevice): + def __init__(self, hive_session, hive_device): """Initialize the Light device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.light_device_type = hivedevice["Hive_Light_DeviceType"] - self.session = hivesession - self.attributes = {} - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self.session.entities.append(self) + super().__init__(hive_session, hive_device) + self.light_device_type = hive_device["Hive_Light_DeviceType"] @property def unique_id(self): @@ -47,11 +43,6 @@ def device_info(self): """Return device information.""" return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the display name of this light.""" @@ -106,6 +97,7 @@ def is_on(self): """Return true if light is on.""" return self.session.light.get_state(self.node_id) + @refresh_system def turn_on(self, **kwargs): """Instruct the light to turn on.""" new_brightness = None @@ -134,14 +126,10 @@ def turn_on(self, **kwargs): new_color, ) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - + @refresh_system def turn_off(self, **kwargs): """Instruct the light to turn off.""" self.session.light.turn_off(self.node_id) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) @property def supported_features(self): diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 886d6841ebb850..2e7c4f4f179dee 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "documentation": "https://www.home-assistant.io/components/hive", "requirements": [ - "pyhiveapi==0.2.18.1" + "pyhiveapi==0.2.19" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index c43fe461a8e6a1..ccd635015de320 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -2,7 +2,7 @@ from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from . import DATA_HIVE, DOMAIN +from . import DOMAIN, DATA_HIVE, HiveEntity FRIENDLY_NAMES = { "Hub_OnlineStatus": "Hive Hub Status", @@ -19,28 +19,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive sensor devices.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - if ( - discovery_info["HA_DeviceType"] == "Hub_OnlineStatus" - or discovery_info["HA_DeviceType"] == "Hive_OutsideTemperature" - ): - add_entities([HiveSensorEntity(session, discovery_info)]) + session = hass.data.get(DATA_HIVE) + devs = [] + for dev in discovery_info: + if dev["HA_DeviceType"] in FRIENDLY_NAMES: + devs.append(HiveSensorEntity(session, dev)) + add_entities(devs) -class HiveSensorEntity(Entity): +class HiveSensorEntity(HiveEntity, Entity): """Hive Sensor Entity.""" - def __init__(self, hivesession, hivedevice): - """Initialize the sensor.""" - self.node_id = hivedevice["Hive_NodeID"] - self.device_type = hivedevice["HA_DeviceType"] - self.node_device_type = hivedevice["Hive_DeviceType"] - self.session = hivesession - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self.session.entities.append(self) - @property def unique_id(self): """Return unique ID of entity.""" @@ -51,11 +41,6 @@ def device_info(self): """Return device information.""" return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the name of the sensor.""" @@ -82,6 +67,4 @@ def icon(self): def update(self): """Update all Node data from Hive.""" - if self.session.core.update_data(self.node_id): - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) + self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml new file mode 100644 index 00000000000000..27d7acfc83bab0 --- /dev/null +++ b/homeassistant/components/hive/services.yaml @@ -0,0 +1,27 @@ +boost_heating: + description: "Set the boost mode ON defining the period of time and the desired target temperature + for the boost." + fields: + entity_id: + { + description: Enter the entity_id for the device required to set the boost mode., + example: "climate.heating", + } + time_period: + { description: Set the time period for the boost., example: "01:30:00" } + temperature: + { + description: Set the target temperature for the boost period., + example: "20.5", + } +boost_hotwater: + description: + "Set the boost mode ON or OFF defining the period of time for the boost." + fields: + entity_id: + { + description: Enter the entity_id for the device reuired to set the boost mode., + example: "water_heater.hot_water", + } + time_period: { description: Set the time period for the boost., example: "01:30:00" } + on_off: { description: Set the boost function on or off., example: "on" } diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 75efdfe3e5d461..1447f5483a4a0e 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,32 +1,24 @@ """Support for the Hive switches.""" from homeassistant.components.switch import SwitchDevice -from . import DATA_HIVE, DOMAIN +from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive switches.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - add_entities([HiveDevicePlug(session, discovery_info)]) + session = hass.data.get(DATA_HIVE) + devs = [] + for dev in discovery_info: + devs.append(HiveDevicePlug(session, dev)) + add_entities(devs) -class HiveDevicePlug(SwitchDevice): +class HiveDevicePlug(HiveEntity, SwitchDevice): """Hive Active Plug.""" - def __init__(self, hivesession, hivedevice): - """Initialize the Switch device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.session = hivesession - self.attributes = {} - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self.session.entities.append(self) - @property def unique_id(self): """Return unique ID of entity.""" @@ -37,11 +29,6 @@ def device_info(self): """Return device information.""" return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the name of this Switch device if any.""" @@ -62,17 +49,15 @@ def is_on(self): """Return true if switch is on.""" return self.session.switch.get_state(self.node_id) + @refresh_system def turn_on(self, **kwargs): """Turn the switch on.""" self.session.switch.turn_on(self.node_id) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) + @refresh_system def turn_off(self, **kwargs): """Turn the device off.""" self.session.switch.turn_off(self.node_id) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) def update(self): """Update all Node data from Hive.""" diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 1b009582c1aec7..c60a9ec01d1fcf 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -1,51 +1,36 @@ """Support for hive water heaters.""" -from homeassistant.const import TEMP_CELSIUS - from homeassistant.components.water_heater import ( STATE_ECO, - STATE_ON, STATE_OFF, + STATE_ON, SUPPORT_OPERATION_MODE, WaterHeaterDevice, ) - -from . import DATA_HIVE, DOMAIN +from homeassistant.const import TEMP_CELSIUS +from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE HIVE_TO_HASS_STATE = {"SCHEDULE": STATE_ECO, "ON": STATE_ON, "OFF": STATE_OFF} - HASS_TO_HIVE_STATE = {STATE_ECO: "SCHEDULE", STATE_ON: "ON", STATE_OFF: "OFF"} - SUPPORT_WATER_HEATER = [STATE_ECO, STATE_ON, STATE_OFF] def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink water heater devices.""" + """Set up the Hive water heater devices.""" if discovery_info is None: return - if discovery_info["HA_DeviceType"] != "HotWater": - return session = hass.data.get(DATA_HIVE) - water_heater = HiveWaterHeater(session, discovery_info) + devs = [] + for dev in discovery_info: + devs.append(HiveWaterHeater(session, dev)) + add_entities(devs) - add_entities([water_heater]) - -class HiveWaterHeater(WaterHeaterDevice): +class HiveWaterHeater(HiveEntity, WaterHeaterDevice): """Hive Water Heater Device.""" - def __init__(self, hivesession, hivedevice): - """Initialize the Water Heater device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.session = hivesession - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self._unit_of_measurement = TEMP_CELSIUS - @property def unique_id(self): """Return unique ID of entity.""" @@ -61,11 +46,6 @@ def supported_features(self): """Return the list of supported features.""" return SUPPORT_FLAGS_HEATER - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the name of the water heater.""" @@ -76,7 +56,7 @@ def name(self): @property def temperature_unit(self): """Return the unit of measurement.""" - return self._unit_of_measurement + return TEMP_CELSIUS @property def current_operation(self): @@ -88,19 +68,12 @@ def operation_list(self): """List of available operation modes.""" return SUPPORT_WATER_HEATER - async def async_added_to_hass(self): - """When entity is added to Home Assistant.""" - await super().async_added_to_hass() - self.session.entities.append(self) - + @refresh_system def set_operation_mode(self, operation_mode): """Set operation mode.""" new_mode = HASS_TO_HIVE_STATE[operation_mode] self.session.hotwater.set_mode(self.node_id, new_mode) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - def update(self): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/requirements_all.txt b/requirements_all.txt index 32983063775dcd..a68f7f71e9c88b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1223,7 +1223,7 @@ pyheos==0.6.0 pyhik==0.2.3 # homeassistant.components.hive -pyhiveapi==0.2.18.1 +pyhiveapi==0.2.19 # homeassistant.components.homematic pyhomematic==0.1.60 From b0df14db1401a9b4450acd80c1aa3b5cfd7f7673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 28 Sep 2019 00:20:00 +0300 Subject: [PATCH 195/296] Bump Travis timeout to 50 minutes (#26978) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 525a4c8e72c1f5..0e9e030128e2a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,4 +30,4 @@ matrix: cache: pip install: pip install -U tox language: python -script: travis_wait 40 tox --develop +script: travis_wait 50 tox --develop From ac634d71f4cc75946012e51f955c6f5292da2aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 28 Sep 2019 03:02:48 +0300 Subject: [PATCH 196/296] Remove no longer needed Python < 3.6 compatibility code (#27024) --- homeassistant/util/async_.py | 130 +---------------------------------- 1 file changed, 3 insertions(+), 127 deletions(-) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index d43658d1584269..6920e0d97f64cd 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -1,23 +1,13 @@ -"""Asyncio backports for Python 3.4.3 compatibility.""" +"""Asyncio backports for Python 3.6 compatibility.""" import concurrent.futures import threading import logging from asyncio import coroutines from asyncio.events import AbstractEventLoop -from asyncio.futures import Future import asyncio from asyncio import ensure_future -from typing import ( - Any, - Union, - Coroutine, - Callable, - Generator, - TypeVar, - Awaitable, - Optional, -) +from typing import Any, Union, Coroutine, Callable, Generator, TypeVar, Awaitable _LOGGER = logging.getLogger(__name__) @@ -40,105 +30,6 @@ def asyncio_run(main: Awaitable[_T], *, debug: bool = False) -> _T: loop.close() -def _set_result_unless_cancelled(fut: Future, result: Any) -> None: - """Set the result only if the Future was not cancelled.""" - if fut.cancelled(): - return - fut.set_result(result) - - -def _set_concurrent_future_state( - concurr: concurrent.futures.Future, source: Union[concurrent.futures.Future, Future] -) -> None: - """Copy state from a future to a concurrent.futures.Future.""" - assert source.done() - if source.cancelled(): - concurr.cancel() - if not concurr.set_running_or_notify_cancel(): - return - exception = source.exception() - if exception is not None: - concurr.set_exception(exception) - else: - result = source.result() - concurr.set_result(result) - - -def _copy_future_state( - source: Union[concurrent.futures.Future, Future], - dest: Union[concurrent.futures.Future, Future], -) -> None: - """Copy state from another Future. - - The other Future may be a concurrent.futures.Future. - """ - assert source.done() - if dest.cancelled(): - return - assert not dest.done() - if source.cancelled(): - dest.cancel() - else: - exception = source.exception() - if exception is not None: - dest.set_exception(exception) - else: - result = source.result() - dest.set_result(result) - - -def _chain_future( - source: Union[concurrent.futures.Future, Future], - destination: Union[concurrent.futures.Future, Future], -) -> None: - """Chain two futures so that when one completes, so does the other. - - The result (or exception) of source will be copied to destination. - If destination is cancelled, source gets cancelled too. - Compatible with both asyncio.Future and concurrent.futures.Future. - """ - if not isinstance(source, (Future, concurrent.futures.Future)): - raise TypeError("A future is required for source argument") - if not isinstance(destination, (Future, concurrent.futures.Future)): - raise TypeError("A future is required for destination argument") - # pylint: disable=protected-access - if isinstance(source, Future): - source_loop: Optional[AbstractEventLoop] = source._loop - else: - source_loop = None - if isinstance(destination, Future): - dest_loop: Optional[AbstractEventLoop] = destination._loop - else: - dest_loop = None - - def _set_state( - future: Union[concurrent.futures.Future, Future], - other: Union[concurrent.futures.Future, Future], - ) -> None: - if isinstance(future, Future): - _copy_future_state(other, future) - else: - _set_concurrent_future_state(future, other) - - def _call_check_cancel( - destination: Union[concurrent.futures.Future, Future] - ) -> None: - if destination.cancelled(): - if source_loop is None or source_loop is dest_loop: - source.cancel() - else: - source_loop.call_soon_threadsafe(source.cancel) - - def _call_set_state(source: Union[concurrent.futures.Future, Future]) -> None: - if dest_loop is None or dest_loop is source_loop: - _set_state(destination, source) - else: - dest_loop.call_soon_threadsafe(_set_state, destination, source) - - destination.add_done_callback(_call_check_cancel) - source.add_done_callback(_call_set_state) - - def run_coroutine_threadsafe( coro: Union[Coroutine, Generator], loop: AbstractEventLoop ) -> concurrent.futures.Future: @@ -150,22 +41,7 @@ def run_coroutine_threadsafe( if ident is not None and ident == threading.get_ident(): raise RuntimeError("Cannot be called from within the event loop") - if not coroutines.iscoroutine(coro): - raise TypeError("A coroutine object is required") - future: concurrent.futures.Future = concurrent.futures.Future() - - def callback() -> None: - """Handle the call to the coroutine.""" - try: - _chain_future(ensure_future(coro, loop=loop), future) - except Exception as exc: # pylint: disable=broad-except - if future.set_running_or_notify_cancel(): - future.set_exception(exc) - else: - _LOGGER.warning("Exception on lost future: ", exc_info=True) - - loop.call_soon_threadsafe(callback) - return future + return asyncio.run_coroutine_threadsafe(coro, loop) def fire_coroutine_threadsafe(coro: Coroutine, loop: AbstractEventLoop) -> None: From ce97c27a7fa0512d4d5251ae4e543384b20be384 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 27 Sep 2019 18:03:15 -0600 Subject: [PATCH 197/296] Fix possible OpenUV exception due to missing data (#26958) --- homeassistant/components/openuv/binary_sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 59f6e4d1c67e47..621950965f6c2e 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -102,6 +102,11 @@ async def async_update(self): if not data: return + for key in ("from_time", "to_time", "from_uv", "to_uv"): + if not data.get(key): + _LOGGER.info("Skipping update due to missing data: %s", key) + return + if self._sensor_type == TYPE_PROTECTION_WINDOW: self._state = ( parse_datetime(data["from_time"]) From 2af34b461a523e008b11ac8105fd3785dd976ce8 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 28 Sep 2019 00:32:10 +0000 Subject: [PATCH 198/296] [ci skip] Translation update --- .../binary_sensor/.translations/ko.json | 48 +++++++ .../binary_sensor/.translations/pl.json | 4 + .../components/ecobee/.translations/lb.json | 13 ++ .../components/ecobee/.translations/pl.json | 25 ++++ .../components/plex/.translations/lb.json | 11 ++ .../components/plex/.translations/no.json | 3 +- .../components/plex/.translations/pl.json | 11 ++ .../transmission/.translations/lb.json | 40 ++++++ .../transmission/.translations/pl.json | 40 ++++++ .../components/zha/.translations/da.json | 4 + .../components/zha/.translations/en.json | 124 +++++++++--------- 11 files changed, 260 insertions(+), 63 deletions(-) create mode 100644 homeassistant/components/binary_sensor/.translations/ko.json create mode 100644 homeassistant/components/ecobee/.translations/lb.json create mode 100644 homeassistant/components/ecobee/.translations/pl.json create mode 100644 homeassistant/components/transmission/.translations/lb.json create mode 100644 homeassistant/components/transmission/.translations/pl.json diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json new file mode 100644 index 00000000000000..02443d449c543f --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -0,0 +1,48 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", + "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc2b5\ub2c8\ub2e4", + "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc2b5\ub2c8\ub2e4", + "is_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uacbc\uc2b5\ub2c8\ub2e4", + "is_moist": "{entity_name} \uc774(\uac00) \uc2b5\ud569\ub2c8\ub2e4", + "is_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc600\uc2b5\ub2c8\ub2e4", + "is_no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", + "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc2b5\ub2c8\ub2e4", + "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "is_not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud569\ub2c8\ub2e4", + "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_not_occupied": "{entity_name} \uc774 (\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", + "is_not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud614\uc2b5\ub2c8\ub2e4", + "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc2b5\ub2c8\ub2e4", + "is_not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud569\ub2c8\ub2e4", + "is_occupied": "{entity_name} \uc774 (\uac00) \uc0ac\uc6a9\uc911\uc785\ub2c8\ub2e4", + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", + "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", + "is_plugged_in": "{entity_name} \uc774(\uac00) \uaf3d\ud614\uc2b5\ub2c8\ub2e4", + "is_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "is_present": "{entity_name} \uc774(\uac00) \uc788\uc2b5\ub2c8\ub2e4", + "is_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json index 139cff2187fcfb..7862644c727826 100644 --- a/homeassistant/components/binary_sensor/.translations/pl.json +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -14,6 +14,10 @@ "is_no_gas": "{entity_name} nie wykrywa gazu", "is_no_light": "{entity_name} nie wykrywa \u015bwiat\u0142a", "is_no_motion": "{entity_name} nie wykrywa ruchu", + "is_no_problem": "{entity_name} nie wykrywa problemu", + "is_no_smoke": "{entity_name} nie wykrywa dymu", + "is_no_sound": "{entity_name} nie wykrywa d\u017awi\u0119ku", + "is_no_vibration": "{entity_name} nie wykrywa wibracji", "is_off": "{entity_name} jest wy\u0142\u0105czone", "is_on": "{entity_name} jest w\u0142\u0105czone" } diff --git a/homeassistant/components/ecobee/.translations/lb.json b/homeassistant/components/ecobee/.translations/lb.json new file mode 100644 index 00000000000000..1982dd40840283 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/lb.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel" + }, + "title": "ecobee API Schl\u00ebssel" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/pl.json b/homeassistant/components/ecobee/.translations/pl.json new file mode 100644 index 00000000000000..5c51d86fee4980 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 ecobee" + }, + "error": { + "pin_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania kodu PIN od ecobee; sprawd\u017a, czy klucz API jest poprawny.", + "token_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania token\u00f3w od ecobee; prosz\u0119 spr\u00f3buj ponownie." + }, + "step": { + "authorize": { + "description": "Autoryzuj t\u0119 aplikacj\u0119 na https://www.ecobee.com/consumerportal/index.html za pomoc\u0105 kodu PIN: \n\n {pin} \n \n Nast\u0119pnie naci\u015bnij przycisk Prze\u015blij.", + "title": "Autoryzuj aplikacj\u0119 na ecobee.com" + }, + "user": { + "data": { + "api_key": "Klucz API" + }, + "description": "Prosz\u0119 wprowadzi\u0107 klucz API uzyskany na ecobee.com.", + "title": "Klucz API" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 244044b2f67a80..1e6488784d409a 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Weis all Kontrollen", + "use_episode_art": "Benotz Biller vun der Episode" + }, + "description": "Optioune fir Plex Medie Spiller" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index 393639dd4c9fc9..f7a6bfd9c7f1b8 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -46,7 +46,8 @@ "step": { "plex_mp_settings": { "data": { - "show_all_controls": "Vis alle kontroller" + "show_all_controls": "Vis alle kontroller", + "use_episode_art": "Bruk episode bilde" }, "description": "Alternativer for Plex Media Players" } diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index 606f97d6965c60..ea1db4ec2f3243 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -29,5 +29,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Poka\u017c wszystkie elementy steruj\u0105ce", + "use_episode_art": "U\u017cyj grafiki episodu" + }, + "description": "Opcje dla odtwarzaczy multimedialnych Plex" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/lb.json b/homeassistant/components/transmission/.translations/lb.json new file mode 100644 index 00000000000000..6cc611fcb71550 --- /dev/null +++ b/homeassistant/components/transmission/.translations/lb.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + }, + "error": { + "cannot_connect": "Kann sech net mam Server verbannen.", + "wrong_credentials": "Falsche Benotzernumm oder Passwuert" + }, + "step": { + "options": { + "data": { + "scan_interval": "Intervalle vun de Mise \u00e0 jour" + }, + "title": "Optioune konfigur\u00e9ieren" + }, + "user": { + "data": { + "host": "Server", + "name": "Numm", + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm" + }, + "title": "Transmission Client ariichten" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle vun de Mise \u00e0 jour" + }, + "description": "Optioune fir Transmission konfigur\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pl.json b/homeassistant/components/transmission/.translations/pl.json new file mode 100644 index 00000000000000..9b45e3107672af --- /dev/null +++ b/homeassistant/components/transmission/.translations/pl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Wymagana jest tylko jedna instancja." + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem", + "wrong_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o" + }, + "step": { + "options": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + }, + "title": "Opcje" + }, + "user": { + "data": { + "host": "Host", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Konfiguracja klienta Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + }, + "description": "Konfiguracja opcji dla Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json index 8d99b6eebc9db8..0b800ecd80a7ef 100644 --- a/homeassistant/components/zha/.translations/da.json +++ b/homeassistant/components/zha/.translations/da.json @@ -18,6 +18,10 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Advare" + }, "trigger_subtype": { "both_buttons": "Begge knapper", "button_1": "F\u00f8rste knap", diff --git a/homeassistant/components/zha/.translations/en.json b/homeassistant/components/zha/.translations/en.json index ea1ad48bbff940..d8e8955a935072 100644 --- a/homeassistant/components/zha/.translations/en.json +++ b/homeassistant/components/zha/.translations/en.json @@ -1,67 +1,67 @@ { - "config": { - "abort": { - "single_instance_allowed": "Only a single configuration of ZHA is allowed." - }, - "error": { - "cannot_connect": "Unable to connect to ZHA device." - }, - "step": { - "user": { - "data": { - "radio_type": "Radio Type", - "usb_path": "USB Device Path" + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of ZHA is allowed." + }, + "error": { + "cannot_connect": "Unable to connect to ZHA device." + }, + "step": { + "user": { + "data": { + "radio_type": "Radio Type", + "usb_path": "USB Device Path" + }, + "title": "ZHA" + } }, "title": "ZHA" - } }, - "title": "ZHA" - }, - "device_automation": { - "action_type": { - "squawk": "Squawk", - "warn": "Warn" - }, - "trigger_subtype": { - "both_buttons": "Both buttons", - "button_1": "First button", - "button_2": "Second button", - "button_3": "Third button", - "button_4": "Fourth button", - "button_5": "Fifth button", - "button_6": "Sixth button", - "close": "Close", - "dim_down": "Dim down", - "dim_up": "Dim up", - "face_1": "with face 1 activated", - "face_2": "with face 2 activated", - "face_3": "with face 3 activated", - "face_4": "with face 4 activated", - "face_5": "with face 5 activated", - "face_6": "with face 6 activated", - "face_any": "With any/specified face(s) activated", - "left": "Left", - "open": "Open", - "right": "Right", - "turn_off": "Turn off", - "turn_on": "Turn on" - }, - "trigger_type": { - "device_dropped": "Device dropped", - "device_flipped": "Device flipped \"{subtype}\"", - "device_knocked": "Device knocked \"{subtype}\"", - "device_rotated": "Device rotated \"{subtype}\"", - "device_shaken": "Device shaken", - "device_slid": "Device slid \"{subtype}\"", - "device_tilted": "Device tilted", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_triple_press": "\"{subtype}\" button triple clicked" + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Warn" + }, + "trigger_subtype": { + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "close": "Close", + "dim_down": "Dim down", + "dim_up": "Dim up", + "face_1": "with face 1 activated", + "face_2": "with face 2 activated", + "face_3": "with face 3 activated", + "face_4": "with face 4 activated", + "face_5": "with face 5 activated", + "face_6": "with face 6 activated", + "face_any": "With any/specified face(s) activated", + "left": "Left", + "open": "Open", + "right": "Right", + "turn_off": "Turn off", + "turn_on": "Turn on" + }, + "trigger_type": { + "device_dropped": "Device dropped", + "device_flipped": "Device flipped \"{subtype}\"", + "device_knocked": "Device knocked \"{subtype}\"", + "device_rotated": "Device rotated \"{subtype}\"", + "device_shaken": "Device shaken", + "device_slid": "Device slid \"{subtype}\"", + "device_tilted": "Device tilted", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_triple_press": "\"{subtype}\" button triple clicked" + } } - } -} +} \ No newline at end of file From 1c72a246a048d0f2f3d3dca0b77be559dae01a78 Mon Sep 17 00:00:00 2001 From: SneakSnackSnake <55602459+SneakSnackSnake@users.noreply.github.com> Date: Sat, 28 Sep 2019 08:15:29 +0200 Subject: [PATCH 199/296] Update pythonegardia to 1.0.40 (#27009) --- homeassistant/components/egardia/manifest.json | 8 ++------ requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/egardia/manifest.json b/homeassistant/components/egardia/manifest.json index 3a95b90db99001..6f103449868ab1 100644 --- a/homeassistant/components/egardia/manifest.json +++ b/homeassistant/components/egardia/manifest.json @@ -2,11 +2,7 @@ "domain": "egardia", "name": "Egardia", "documentation": "https://www.home-assistant.io/components/egardia", - "requirements": [ - "pythonegardia==1.0.39" - ], + "requirements": ["pythonegardia==1.0.40"], "dependencies": [], - "codeowners": [ - "@jeroenterheerdt" - ] + "codeowners": ["@jeroenterheerdt"] } diff --git a/requirements_all.txt b/requirements_all.txt index a68f7f71e9c88b..bd0a11f3739751 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1585,7 +1585,7 @@ python_awair==0.0.4 python_opendata_transport==0.1.4 # homeassistant.components.egardia -pythonegardia==1.0.39 +pythonegardia==1.0.40 # homeassistant.components.tile pytile==2.0.6 From 2dfdc5f6f884d3bbb5729bcdaf811a30ae46b8f9 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Sat, 28 Sep 2019 05:32:22 -0400 Subject: [PATCH 200/296] Improve ecobee service schemas (#26955) * Validate date and time in create vaction Improve validation with utility functions. * Improve validate ATTR_VACATION_NAME * Add tests for ecobee.util functions * Revise tests as standalone functions --- homeassistant/components/ecobee/climate.py | 17 +++++++---- homeassistant/components/ecobee/util.py | 21 +++++++++++++ tests/components/ecobee/test_util.py | 35 ++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/ecobee/util.py create mode 100644 tests/components/ecobee/test_util.py diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 460bd2bb4a4ecf..6eccdccf8c6ac0 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -37,6 +37,7 @@ import homeassistant.helpers.config_validation as cv from .const import DOMAIN, _LOGGER +from .util import ecobee_date, ecobee_time ATTR_COOL_TEMP = "cool_temp" ATTR_END_DATE = "end_date" @@ -106,13 +107,17 @@ CREATE_VACATION_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_VACATION_NAME): cv.string, + vol.Required(ATTR_VACATION_NAME): vol.All(cv.string, vol.Length(max=12)), vol.Required(ATTR_COOL_TEMP): vol.Coerce(float), vol.Required(ATTR_HEAT_TEMP): vol.Coerce(float), - vol.Inclusive(ATTR_START_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, - vol.Inclusive(ATTR_START_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, - vol.Inclusive(ATTR_END_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, - vol.Inclusive(ATTR_END_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, + vol.Inclusive( + ATTR_START_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG + ): ecobee_date, + vol.Inclusive( + ATTR_START_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG + ): ecobee_time, + vol.Inclusive(ATTR_END_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): ecobee_date, + vol.Inclusive(ATTR_END_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): ecobee_time, vol.Optional(ATTR_FAN_MODE, default="auto"): vol.Any("auto", "on"), vol.Optional(ATTR_FAN_MIN_ON_TIME, default=0): vol.All( int, vol.Range(min=0, max=60) @@ -123,7 +128,7 @@ DELETE_VACATION_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_VACATION_NAME): cv.string, + vol.Required(ATTR_VACATION_NAME): vol.All(cv.string, vol.Length(max=12)), } ) diff --git a/homeassistant/components/ecobee/util.py b/homeassistant/components/ecobee/util.py new file mode 100644 index 00000000000000..3acc3e5676d8d6 --- /dev/null +++ b/homeassistant/components/ecobee/util.py @@ -0,0 +1,21 @@ +"""Validation utility functions for ecobee services.""" +from datetime import datetime +import voluptuous as vol + + +def ecobee_date(date_string): + """Validate a date_string as valid for the ecobee API.""" + try: + datetime.strptime(date_string, "%Y-%m-%d") + except ValueError: + raise vol.Invalid("Date does not match ecobee date format YYYY-MM-DD") + return date_string + + +def ecobee_time(time_string): + """Validate a time_string as valid for the ecobee API.""" + try: + datetime.strptime(time_string, "%H:%M:%S") + except ValueError: + raise vol.Invalid("Time does not match ecobee 24-hour time format HH:MM:SS") + return time_string diff --git a/tests/components/ecobee/test_util.py b/tests/components/ecobee/test_util.py new file mode 100644 index 00000000000000..ee02f2a33aa7f7 --- /dev/null +++ b/tests/components/ecobee/test_util.py @@ -0,0 +1,35 @@ +"""Tests for the ecobee.util module.""" +import pytest +import voluptuous as vol + +from homeassistant.components.ecobee.util import ecobee_date, ecobee_time + + +def test_ecobee_date_with_valid_input(): + """Test that the date function returns the expected result.""" + test_input = "2019-09-27" + + assert ecobee_date(test_input) == test_input + + +def test_ecobee_date_with_invalid_input(): + """Test that the date function raises the expected exception.""" + test_input = "20190927" + + with pytest.raises(vol.Invalid): + ecobee_date(test_input) + + +def test_ecobee_time_with_valid_input(): + """Test that the time function returns the expected result.""" + test_input = "20:55:15" + + assert ecobee_time(test_input) == test_input + + +def test_ecobee_time_with_invalid_input(): + """Test that the time function raises the expected exception.""" + test_input = "20:55" + + with pytest.raises(vol.Invalid): + ecobee_time(test_input) From f9ac204cc546250d997640965d7797850ead7f8f Mon Sep 17 00:00:00 2001 From: Florian Klien Date: Sat, 28 Sep 2019 11:33:48 +0200 Subject: [PATCH 201/296] Add more providers, bump yessssms version to 0.4.1 (#26874) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bump yessssms version to 0.4.0 adds 'provider' config parameter adds support for providers: * billitel * EDUCOM * fenercell * georg * goood * kronemobile * kuriermobil * SIMfonie * teleplanet * WOWWW * yooopi * black formatting * moved CONF_PROVIDER to component * black formatting * moved error handling on init to get_service * return None, init logging moved to get_service * moved YesssSMS import to top of module * test login data on init. add flag for login data test. removed KeyError * catch connection error, remove CONF_TEST_LOGIN_DATA config flag * requirements updated * lint * lint: use getters for protected members, bump version to 0.4.1b4 * requirements updated to 0.4.1b4 * fix logging messages, info to warning, clear up login_data check * change valid login data message to debug * fix tests * add tests for get_service * bump yessssms version 0.4.1 * tests for get_service refurbished * test refactoring with fixtures * polish fixtures ✨ * replace Mock with patch 🔄 * tiny string fixes, removed unused return_value 🐈 --- homeassistant/components/yessssms/const.py | 3 + .../components/yessssms/manifest.json | 2 +- homeassistant/components/yessssms/notify.py | 52 ++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/yessssms/test_notify.py | 148 +++++++++++++++++- 6 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/yessssms/const.py diff --git a/homeassistant/components/yessssms/const.py b/homeassistant/components/yessssms/const.py new file mode 100644 index 00000000000000..473cdfff1e0257 --- /dev/null +++ b/homeassistant/components/yessssms/const.py @@ -0,0 +1,3 @@ +"""Const for YesssSMS.""" + +CONF_PROVIDER = "provider" diff --git a/homeassistant/components/yessssms/manifest.json b/homeassistant/components/yessssms/manifest.json index 103a9fce31ede2..c7b5535d03c6f6 100644 --- a/homeassistant/components/yessssms/manifest.json +++ b/homeassistant/components/yessssms/manifest.json @@ -3,7 +3,7 @@ "name": "Yessssms", "documentation": "https://www.home-assistant.io/components/yessssms", "requirements": [ - "YesssSMS==0.2.3" + "YesssSMS==0.4.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/yessssms/notify.py b/homeassistant/components/yessssms/notify.py index 28c02080f16621..1c1eed0e89d18c 100644 --- a/homeassistant/components/yessssms/notify.py +++ b/homeassistant/components/yessssms/notify.py @@ -3,11 +3,16 @@ import voluptuous as vol +from YesssSMS import YesssSMS + from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService + +from .const import CONF_PROVIDER + _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -15,27 +20,52 @@ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_RECIPIENT): cv.string, + vol.Optional(CONF_PROVIDER, default="YESSS"): cv.string, } ) def get_service(hass, config, discovery_info=None): """Get the YesssSMS notification service.""" - return YesssSMSNotificationService( - config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_RECIPIENT] + + try: + yesss = YesssSMS( + config[CONF_USERNAME], config[CONF_PASSWORD], provider=config[CONF_PROVIDER] + ) + except YesssSMS.UnsupportedProviderError as ex: + _LOGGER.error("Unknown provider: %s", ex) + return None + try: + if not yesss.login_data_valid(): + _LOGGER.error( + "Login data is not valid! Please double check your login data at %s", + yesss.get_login_url(), + ) + return None + + _LOGGER.debug("Login data for '%s' valid", yesss.get_provider()) + except YesssSMS.ConnectionError: + _LOGGER.warning( + "Connection Error, could not verify login data for '%s'", + yesss.get_provider(), + ) + pass + + _LOGGER.debug( + "initialized; library version: %s, with %s", + yesss.version(), + yesss.get_provider(), ) + return YesssSMSNotificationService(yesss, config[CONF_RECIPIENT]) class YesssSMSNotificationService(BaseNotificationService): """Implement a notification service for the YesssSMS service.""" - def __init__(self, username, password, recipient): + def __init__(self, client, recipient): """Initialize the service.""" - from YesssSMS import YesssSMS - - self.yesss = YesssSMS(username, password) + self.yesss = client self._recipient = recipient - _LOGGER.debug("initialized; library version: %s", self.yesss.version()) def send_message(self, message="", **kwargs): """Send a SMS message via Yesss.at's website.""" @@ -56,10 +86,12 @@ def send_message(self, message="", **kwargs): except self.yesss.EmptyMessageError as ex: _LOGGER.error("Cannot send empty SMS message: %s", ex) except self.yesss.SMSSendingError as ex: - _LOGGER.error(str(ex), exc_info=ex) - except ConnectionError as ex: + _LOGGER.error(ex) + except self.yesss.ConnectionError as ex: _LOGGER.error( - "YesssSMS: unable to connect to yesss.at server.", exc_info=ex + "Unable to connect to server of provider (%s): %s", + self.yesss.get_provider(), + ex, ) except self.yesss.AccountSuspendedError as ex: _LOGGER.error( diff --git a/requirements_all.txt b/requirements_all.txt index bd0a11f3739751..53f72801cc363b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -100,7 +100,7 @@ TwitterAPI==2.5.9 WazeRouteCalculator==0.10 # homeassistant.components.yessssms -YesssSMS==0.2.3 +YesssSMS==0.4.1 # homeassistant.components.abode abodepy==0.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a860c67dbd1368..7ac46e96cd4d78 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.1.3 PyTransportNSW==0.1.1 # homeassistant.components.yessssms -YesssSMS==0.2.3 +YesssSMS==0.4.1 # homeassistant.components.adguard adguardhome==0.2.1 diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py index 3d11cdedc67bd7..5cc204ccc4d4ca 100644 --- a/tests/components/yessssms/test_notify.py +++ b/tests/components/yessssms/test_notify.py @@ -1,7 +1,148 @@ """The tests for the notify yessssms platform.""" import unittest +from unittest.mock import patch + +import pytest import requests_mock + +from homeassistant.setup import async_setup_component import homeassistant.components.yessssms.notify as yessssms +from homeassistant.components.yessssms.const import CONF_PROVIDER + +from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_USERNAME + + +@pytest.fixture(name="config") +def config_data(): + """Set valid config data.""" + config = { + "notify": { + "platform": "yessssms", + "name": "sms", + CONF_USERNAME: "06641234567", + CONF_PASSWORD: "secretPassword", + CONF_RECIPIENT: "06509876543", + CONF_PROVIDER: "educom", + } + } + return config + + +@pytest.fixture(name="valid_settings") +def init_valid_settings(hass, config): + """Initialize component with valid settings.""" + return async_setup_component(hass, "notify", config) + + +@pytest.fixture(name="invalid_provider_settings") +def init_invalid_provider_settings(hass, config): + """Set invalid provider data and initalize component.""" + config["notify"][CONF_PROVIDER] = "FantasyMobile" # invalid provider + return async_setup_component(hass, "notify", config) + + +@pytest.fixture(name="invalid_login_data") +def mock_invalid_login_data(): + """Mock invalid login data.""" + path = "homeassistant.components.yessssms.notify.YesssSMS.login_data_valid" + with patch(path, return_value=False): + yield + + +@pytest.fixture(name="valid_login_data") +def mock_valid_login_data(): + """Mock valid login data.""" + path = "homeassistant.components.yessssms.notify.YesssSMS.login_data_valid" + with patch(path, return_value=True): + yield + + +@pytest.fixture(name="connection_error") +def mock_connection_error(): + """Mock a connection error.""" + path = "homeassistant.components.yessssms.notify.YesssSMS.login_data_valid" + with patch(path, side_effect=yessssms.YesssSMS.ConnectionError()): + yield + + +async def test_unsupported_provider_error(hass, caplog, invalid_provider_settings): + """Test for error on unsupported provider.""" + await invalid_provider_settings + for record in caplog.records: + if ( + record.levelname == "ERROR" + and record.name == "homeassistant.components.yessssms.notify" + ): + assert ( + "Unknown provider: provider (fantasymobile) is not known to YesssSMS" + in record.message + ) + assert ( + "Unknown provider: provider (fantasymobile) is not known to YesssSMS" + in caplog.text + ) + assert not hass.services.has_service("notify", "sms") + + +async def test_false_login_data_error(hass, caplog, valid_settings, invalid_login_data): + """Test login data check error.""" + await valid_settings + assert not hass.services.has_service("notify", "sms") + for record in caplog.records: + if ( + record.levelname == "ERROR" + and record.name == "homeassistant.components.yessssms.notify" + ): + assert ( + "Login data is not valid! Please double check your login data at" + in record.message + ) + + +async def test_init_success(hass, caplog, valid_settings, valid_login_data): + """Test for successful init of yessssms.""" + await valid_settings + assert hass.services.has_service("notify", "sms") + messages = [] + for record in caplog.records: + if ( + record.levelname == "DEBUG" + and record.name == "homeassistant.components.yessssms.notify" + ): + messages.append(record.message) + assert "Login data for 'educom' valid" in messages[0] + assert ( + "initialized; library version: {}".format(yessssms.YesssSMS("", "").version()) + in messages[1] + ) + + +async def test_connection_error_on_init(hass, caplog, valid_settings, connection_error): + """Test for connection error on init.""" + await valid_settings + assert hass.services.has_service("notify", "sms") + for record in caplog.records: + if ( + record.levelname == "WARNING" + and record.name == "homeassistant.components.yessssms.notify" + ): + assert ( + "Connection Error, could not verify login data for '{}'".format( + "educom" + ) + in record.message + ) + for record in caplog.records: + if ( + record.levelname == "DEBUG" + and record.name == "homeassistant.components.yessssms.notify" + ): + assert ( + "initialized; library version: {}".format( + yessssms.YesssSMS("", "").version() + ) + in record.message + ) class TestNotifyYesssSMS(unittest.TestCase): @@ -12,7 +153,8 @@ def setUp(self): # pylint: disable=invalid-name login = "06641234567" passwd = "testpasswd" recipient = "06501234567" - self.yessssms = yessssms.YesssSMSNotificationService(login, passwd, recipient) + client = yessssms.YesssSMS(login, passwd) + self.yessssms = yessssms.YesssSMSNotificationService(client, recipient) @requests_mock.Mocker() def test_login_error(self, mock): @@ -197,7 +339,7 @@ def test_connection_error(self, mock): "POST", # pylint: disable=protected-access self.yessssms.yesss._login_url, - exc=ConnectionError, + exc=yessssms.YesssSMS.ConnectionError, ) message = "Testing YesssSMS platform :)" @@ -209,4 +351,4 @@ def test_connection_error(self, mock): self.assertTrue(mock.called) self.assertEqual(mock.call_count, 1) - self.assertIn("unable to connect", context.output[0]) + self.assertIn("cannot connect to provider", context.output[0]) From f3d408aca4e707f62ccd4902001b550cbe1a8148 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Sat, 28 Sep 2019 13:13:12 +0200 Subject: [PATCH 202/296] Upgrade youtube_dl to 2019.09.28 (#27031) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 4e253741b051f8..71e1a81135a662 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,7 +3,7 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/components/media_extractor", "requirements": [ - "youtube_dl==2019.09.12.1" + "youtube_dl==2019.09.28" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 53f72801cc363b..ab99704f9653cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2005,7 +2005,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.09.12.1 +youtube_dl==2019.09.28 # homeassistant.components.zengge zengge==0.2 From 6d773198a12f399cb9d89795d27fa4c5023c8734 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Sat, 28 Sep 2019 21:53:16 +1000 Subject: [PATCH 203/296] Add availability_template to Template Cover platform (#26509) * Added availability_template to Template Cover platform * Added to test for invalid values in availability_template * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Moved availability_template rendering to common loop * Removed 'Magic' string and removed duplicate code * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (async, magic values and state checks) --- homeassistant/components/template/cover.py | 39 +++++--- tests/components/template/test_cover.py | 109 +++++++++++++++++++++ 2 files changed, 137 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 51b9a523b3bbe2..483ee1ae8723fb 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -38,6 +38,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_OPEN, STATE_CLOSED, "true", "false"] @@ -74,6 +75,7 @@ vol.Exclusive( CONF_VALUE_TEMPLATE, CONF_VALUE_OR_POSITION_TEMPLATE ): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_TILT_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, @@ -103,6 +105,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= position_template = device_config.get(CONF_POSITION_TEMPLATE) tilt_template = device_config.get(CONF_TILT_TEMPLATE) icon_template = device_config.get(CONF_ICON_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) device_class = device_config.get(CONF_DEVICE_CLASS) open_action = device_config.get(OPEN_ACTION) @@ -144,6 +147,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if str(temp_ids) != MATCH_ALL: template_entity_ids |= set(temp_ids) + if availability_template is not None: + temp_ids = availability_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + if not template_entity_ids: template_entity_ids = MATCH_ALL @@ -160,6 +168,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= tilt_template, icon_template, entity_picture_template, + availability_template, open_action, close_action, stop_action, @@ -192,6 +201,7 @@ def __init__( tilt_template, icon_template, entity_picture_template, + availability_template, open_action, close_action, stop_action, @@ -213,6 +223,7 @@ def __init__( self._icon_template = icon_template self._device_class = device_class self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._open_script = None if open_action is not None: self._open_script = Script(hass, open_action) @@ -235,6 +246,7 @@ def __init__( self._position = None self._tilt_value = None self._entities = entity_ids + self._available = True if self._template is not None: self._template.hass = self.hass @@ -246,6 +258,8 @@ def __init__( self._icon_template.hass = self.hass if self._entity_picture_template is not None: self._entity_picture_template.hass = self.hass + if self._availability_template is not None: + self._availability_template.hass = self.hass async def async_added_to_hass(self): """Register callbacks.""" @@ -332,6 +346,11 @@ def should_poll(self): """Return the polling state.""" return False + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_open_cover(self, **kwargs): """Move the cover up.""" if self._open_script: @@ -430,11 +449,8 @@ async def async_update(self): ) else: self._position = state - except TemplateError as ex: - _LOGGER.error(ex) - self._position = None - except ValueError as ex: - _LOGGER.error(ex) + except (TemplateError, ValueError) as err: + _LOGGER.error(err) self._position = None if self._tilt_template is not None: try: @@ -447,22 +463,23 @@ async def async_update(self): ) else: self._tilt_value = state - except TemplateError as ex: - _LOGGER.error(ex) - self._tilt_value = None - except ValueError as ex: - _LOGGER.error(ex) + except (TemplateError, ValueError) as err: + _LOGGER.error(err) self._tilt_value = None for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), + ("_available", self._availability_template), ): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 247ee25027c94f..d3be01cbdc3ae8 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -16,7 +16,10 @@ SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, STATE_CLOSED, + STATE_UNAVAILABLE, STATE_OPEN, + STATE_ON, + STATE_OFF, ) from tests.common import assert_setup_component, async_mock_service @@ -839,6 +842,112 @@ async def test_entity_picture_template(hass, calls): assert state.attributes["entity_picture"] == "/local/cover.png" +async def test_availability_template(hass, calls): + """Test availability template.""" + with assert_setup_component(1, "cover"): + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + "availability_template": "{{ is_state('availability_state.state','on') }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get("cover.test_template_cover").state == STATE_UNAVAILABLE + + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get("cover.test_template_cover").state != STATE_UNAVAILABLE + + +async def test_availability_without_availability_template(hass, calls): + """Test that component is availble if there is no.""" + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("cover.test_template_cover") + assert state.state != STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "availability_template": "{{ x - 12 }}", + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("cover.test_template_cover") != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text + + async def test_device_class(hass, calls): """Test device class.""" with assert_setup_component(1, "cover"): From 5c5f6a21af3193c7f41827d28b94bd8734130261 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Sat, 28 Sep 2019 21:55:29 +1000 Subject: [PATCH 204/296] Add availability_template to Template Binary Sensor platform (#26510) * Added availability_template to Template Binary Sensor platform * Added to test for invalid values in availability_template * black * simplified exception handler * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Moved availability_template rendering to common loop * Removed 'Magic' string * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (magic values and state checks) --- .../components/template/binary_sensor.py | 29 ++++-- .../components/template/test_binary_sensor.py | 89 ++++++++++++++++++- 2 files changed, 111 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index e0fc867720010c..d5ade703c97440 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -26,6 +26,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change, async_track_same_state +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -38,6 +39,7 @@ vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_ATTRIBUTE_TEMPLATES): vol.Schema({cv.string: cv.template}), vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -60,6 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= value_template = device_config[CONF_VALUE_TEMPLATE] icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) @@ -70,6 +73,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_VALUE_TEMPLATE: value_template, CONF_ICON_TEMPLATE: icon_template, CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, } for tpl_name, template in chain(templates.items(), attribute_templates.items()): @@ -117,6 +121,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= value_template, icon_template, entity_picture_template, + availability_template, entity_ids, delay_on, delay_off, @@ -143,6 +148,7 @@ def __init__( value_template, icon_template, entity_picture_template, + availability_template, entity_ids, delay_on, delay_off, @@ -156,12 +162,14 @@ def __init__( self._template = value_template self._state = None self._icon_template = icon_template + self._availability_template = availability_template self._entity_picture_template = entity_picture_template self._icon = None self._entity_picture = None self._entities = entity_ids self._delay_on = delay_on self._delay_off = delay_off + self._available = True self._attribute_templates = attribute_templates self._attributes = {} @@ -223,6 +231,11 @@ def should_poll(self): """No polling needed.""" return False + @property + def available(self): + """Availability indicator.""" + return self._available + @callback def _async_render(self): """Get the state of template.""" @@ -240,11 +253,6 @@ def _async_render(self): return _LOGGER.error("Could not render template %s: %s", self._name, ex) - templates = { - "_icon": self._icon_template, - "_entity_picture": self._entity_picture_template, - } - attrs = {} if self._attribute_templates is not None: for key, value in self._attribute_templates.items(): @@ -254,12 +262,21 @@ def _async_render(self): _LOGGER.error("Error rendering attribute %s: %s", key, err) self._attributes = attrs + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + "_available": self._availability_template, + } + for property_name, template in templates.items(): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index c8cec168d6e6cd..143811da2099e5 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -3,7 +3,13 @@ import unittest from unittest import mock -from homeassistant.const import MATCH_ALL, EVENT_HOMEASSISTANT_START +from homeassistant.const import ( + MATCH_ALL, + EVENT_HOMEASSISTANT_START, + STATE_UNAVAILABLE, + STATE_ON, + STATE_OFF, +) from homeassistant import setup from homeassistant.components.template import binary_sensor as template from homeassistant.exceptions import TemplateError @@ -238,6 +244,7 @@ def test_attributes(self): template_hlpr.Template("{{ 1 > 1 }}", self.hass), None, None, + None, MATCH_ALL, None, None, @@ -298,6 +305,7 @@ def test_update_template_error(self, mock_render): template_hlpr.Template("{{ 1 > 1 }}", self.hass), None, None, + None, MATCH_ALL, None, None, @@ -428,6 +436,59 @@ async def test_template_delay_off(hass): assert state.state == "on" +async def test_available_without_availability_template(hass): + """Ensure availability is true without an availability_template.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "true", + "device_class": "motion", + "delay_off": 5, + } + }, + } + } + await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE + + +async def test_availability_template(hass): + """Test availability template.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "true", + "device_class": "motion", + "delay_off": 5, + "availability_template": "{{ is_state('sensor.test_state','on') }}", + } + }, + } + } + await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_state", STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state == STATE_UNAVAILABLE + + hass.states.async_set("sensor.test_state", STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE + + async def test_invalid_attribute_template(hass, caplog): """Test that errors are logged if rendering template fails.""" hass.states.async_set("binary_sensor.test_sensor", "true") @@ -458,6 +519,32 @@ async def test_invalid_attribute_template(hass, caplog): assert ("Error rendering attribute test_attribute") in caplog.text +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + + await setup.async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "template", + "sensors": { + "my_sensor": { + "value_template": "{{ states.binary_sensor.test_sensor }}", + "availability_template": "{{ x - 12 }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.my_sensor").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text + + async def test_no_update_template_match_all(hass, caplog): """Test that we do not update sensors that match on all.""" hass.states.async_set("binary_sensor.test_sensor", "true") From 74196eaf8b0538f55a7477a00dcec1ba79c4c71c Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Sat, 28 Sep 2019 21:59:40 +1000 Subject: [PATCH 205/296] Add availability_template to Template Fan platform (#26511) * Added availability_template to Template Fan platform * Added to test for invalid values in availability_template * fixed component ID in test * Made availability_template redering erorr more concise * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Removed 'Magic' string * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (magic values and state checks) --- homeassistant/components/template/fan.py | 29 +++++++++ tests/components/template/test_fan.py | 80 +++++++++++++++++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 7fd8c4d9b3cea6..42790e618d96ec 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -33,6 +33,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -58,6 +59,7 @@ vol.Optional(CONF_SPEED_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, @@ -86,6 +88,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_template = device_config.get(CONF_SPEED_TEMPLATE) oscillating_template = device_config.get(CONF_OSCILLATING_TEMPLATE) direction_template = device_config.get(CONF_DIRECTION_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] @@ -103,6 +106,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_template, oscillating_template, direction_template, + availability_template, ): if template is None: continue @@ -131,6 +135,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_template, oscillating_template, direction_template, + availability_template, on_action, off_action, set_speed_action, @@ -156,6 +161,7 @@ def __init__( speed_template, oscillating_template, direction_template, + availability_template, on_action, off_action, set_speed_action, @@ -175,6 +181,8 @@ def __init__( self._speed_template = speed_template self._oscillating_template = oscillating_template self._direction_template = direction_template + self._availability_template = availability_template + self._available = True self._supported_features = 0 self._on_script = Script(hass, on_action) @@ -207,6 +215,8 @@ def __init__( if self._direction_template: self._direction_template.hass = self.hass self._supported_features |= SUPPORT_DIRECTION + if self._availability_template: + self._availability_template.hass = self.hass self._entities = entity_ids # List of valid speeds @@ -252,6 +262,11 @@ def should_poll(self): """Return the polling state.""" return False + @property + def available(self): + """Return availability of Device.""" + return self._available + # pylint: disable=arguments-differ async def async_turn_on(self, speed: str = None) -> None: """Turn on the fan.""" @@ -422,3 +437,17 @@ async def async_update(self): ", ".join(_VALID_DIRECTIONS), ) self._direction = None + + # Update Availability if 'availability_template' is defined + if self._availability_template is not None: + try: + self._available = ( + self._availability_template.async_render().lower() == "true" + ) + except (TemplateError, ValueError) as ex: + _LOGGER.error( + "Could not render %s template %s: %s", + CONF_AVAILABILITY_TEMPLATE, + self._name, + ex, + ) diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index b80522b37e24c5..5753684795b127 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.fan import ( ATTR_SPEED, ATTR_OSCILLATING, @@ -26,6 +26,8 @@ _TEST_FAN = "fan.test_fan" # Represent for fan's state _STATE_INPUT_BOOLEAN = "input_boolean.state" +# Represent for fan's state +_STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" # Represent for fan's speed _SPEED_INPUT_SELECT = "input_select.speed" # Represent for fan's oscillating @@ -214,6 +216,49 @@ async def test_templates_with_entities(hass, calls): _verify(hass, STATE_ON, SPEED_MEDIUM, True, DIRECTION_FORWARD) +async def test_availability_template_with_entities(hass, calls): + """Test availability tempalates with values from other entities.""" + + with assert_setup_component(1, "fan"): + assert await setup.async_setup_component( + hass, + "fan", + { + "fan": { + "platform": "template", + "fans": { + "test_fan": { + "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", + "value_template": "{{ 'on' }}", + "speed_template": "{{ 'medium' }}", + "oscillating_template": "{{ 1 == 1 }}", + "direction_template": "{{ 'forward' }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get(_TEST_FAN).state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get(_TEST_FAN).state == STATE_UNAVAILABLE + + async def test_templates_with_valid_values(hass, calls): """Test templates with valid values.""" with assert_setup_component(1, "fan"): @@ -272,6 +317,39 @@ async def test_templates_invalid_values(hass, calls): _verify(hass, STATE_OFF, None, None, None) +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + + with assert_setup_component(1, "fan"): + assert await setup.async_setup_component( + hass, + "fan", + { + "fan": { + "platform": "template", + "fans": { + "test_fan": { + "value_template": "{{ 'on' }}", + "availability_template": "{{ x - 12 }}", + "speed_template": "{{ states('input_select.speed') }}", + "oscillating_template": "{{ states('input_select.osc') }}", + "direction_template": "{{ states('input_select.direction') }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("fan.test_fan").state != STATE_UNAVAILABLE + assert ("Could not render availability_template template") in caplog.text + assert ("UndefinedError: 'x' is undefined") in caplog.text + + # End of template tests # From ed82ec5d8e5293746dd288827da21afc942d835b Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Sat, 28 Sep 2019 22:01:18 +1000 Subject: [PATCH 206/296] Add availability_template to Template Light platform (#26512) * Added availability_template to Template Light platform * Added to test for invalid values in availability_template * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Moved availability_template rendering to common loop * Removed 'Magic' string * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (async, magic values and state checks) --- homeassistant/components/template/light.py | 31 ++++++- tests/components/template/test_light.py | 94 +++++++++++++++++++++- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 320dcd2e22feb1..552c21f170dbae 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -28,6 +28,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] @@ -44,6 +45,7 @@ vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_LEVEL_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_LEVEL_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME): cv.string, @@ -65,6 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config.get(CONF_VALUE_TEMPLATE) icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] level_action = device_config.get(CONF_LEVEL_ACTION) @@ -92,6 +95,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if str(temp_ids) != MATCH_ALL: template_entity_ids |= set(temp_ids) + if availability_template is not None: + temp_ids = availability_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + if not template_entity_ids: template_entity_ids = MATCH_ALL @@ -105,6 +113,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, level_action, @@ -132,6 +141,7 @@ def __init__( state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, level_action, @@ -147,6 +157,7 @@ def __init__( self._template = state_template self._icon_template = icon_template self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._on_script = Script(hass, on_action) self._off_script = Script(hass, off_action) self._level_script = None @@ -159,6 +170,7 @@ def __init__( self._entity_picture = None self._brightness = None self._entities = entity_ids + self._available = True if self._template is not None: self._template.hass = self.hass @@ -168,6 +180,8 @@ def __init__( self._icon_template.hass = self.hass if self._entity_picture_template is not None: self._entity_picture_template.hass = self.hass + if self._availability_template is not None: + self._availability_template.hass = self.hass @property def brightness(self): @@ -207,6 +221,11 @@ def entity_picture(self): """Return the entity picture to use in the frontend, if any.""" return self._entity_picture + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_added_to_hass(self): """Register callbacks.""" @@ -218,7 +237,11 @@ def template_light_state_listener(entity, old_state, new_state): @callback def template_light_startup(event): """Update template on startup.""" - if self._template is not None or self._level_template is not None: + if ( + self._template is not None + or self._level_template is not None + or self._availability_template is not None + ): async_track_state_change( self.hass, self._entities, template_light_state_listener ) @@ -298,12 +321,16 @@ async def async_update(self): for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), + ("_available", self._availability_template), ): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 87fd8cd4db3f34..c2dd49a76fbd8a 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -4,13 +4,16 @@ from homeassistant.core import callback from homeassistant import setup from homeassistant.components.light import ATTR_BRIGHTNESS -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from tests.common import get_test_home_assistant, assert_setup_component from tests.components.light import common _LOGGER = logging.getLogger(__name__) +# Represent for light's availability +_STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" + class TestTemplateLight: """Test the Template light.""" @@ -774,3 +777,92 @@ def test_entity_picture_template(self): state = self.hass.states.get("light.test_template_light") assert state.attributes["entity_picture"] == "/local/light.png" + + +async def test_available_template_with_entities(hass): + """Test availability templates with values from other entities.""" + + await setup.async_setup_component( + hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, + }, + } + }, + } + }, + ) + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get("light.test_template_light").state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get("light.test_template_light").state == STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + await setup.async_setup_component( + hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "availability_template": "{{ x - 12 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("light.test_template_light").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text From 11c9bab07810074ee60b21e055fd3cd05a6ed787 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Sat, 28 Sep 2019 22:02:46 +1000 Subject: [PATCH 207/296] Add availability_template to Template Vacuum platform (#26514) * Added availability_template to Template Vacuum platform * Added to test for invalid values in availability_template * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Removed 'Magic' string * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (async, magic values and state checks) --- homeassistant/components/template/vacuum.py | 27 +++++++++ tests/components/template/test_vacuum.py | 64 ++++++++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 5374247daccd70..6a6523514c489a 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -44,6 +44,8 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE + _LOGGER = logging.getLogger(__name__) CONF_VACUUMS = "vacuums" @@ -67,6 +69,7 @@ vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_BATTERY_LEVEL_TEMPLATE): cv.template, vol.Optional(CONF_FAN_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA, vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA, vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA, @@ -94,6 +97,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config.get(CONF_VALUE_TEMPLATE) battery_level_template = device_config.get(CONF_BATTERY_LEVEL_TEMPLATE) fan_speed_template = device_config.get(CONF_FAN_SPEED_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) start_action = device_config[SERVICE_START] pause_action = device_config.get(SERVICE_PAUSE) @@ -113,6 +117,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= (CONF_VALUE_TEMPLATE, state_template), (CONF_BATTERY_LEVEL_TEMPLATE, battery_level_template), (CONF_FAN_SPEED_TEMPLATE, fan_speed_template), + (CONF_AVAILABILITY_TEMPLATE, availability_template), ): if template is None: continue @@ -152,6 +157,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, battery_level_template, fan_speed_template, + availability_template, start_action, pause_action, stop_action, @@ -178,6 +184,7 @@ def __init__( state_template, battery_level_template, fan_speed_template, + availability_template, start_action, pause_action, stop_action, @@ -198,6 +205,7 @@ def __init__( self._template = state_template self._battery_level_template = battery_level_template self._fan_speed_template = fan_speed_template + self._availability_template = availability_template self._supported_features = SUPPORT_START self._start_script = Script(hass, start_action) @@ -235,6 +243,7 @@ def __init__( self._state = None self._battery_level = None self._fan_speed = None + self._available = True if self._template: self._supported_features |= SUPPORT_STATE @@ -280,6 +289,11 @@ def should_poll(self): """Return the polling state.""" return False + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_start(self): """Start or resume the cleaning task.""" await self._start_script.async_run(context=self._context) @@ -421,3 +435,16 @@ async def async_update(self): self._fan_speed_list, ) self._fan_speed = None + # Update availability if availability template is defined + if self._availability_template is not None: + try: + self._available = ( + self._availability_template.async_render().lower() == "true" + ) + except (TemplateError, ValueError) as ex: + _LOGGER.error( + "Could not render %s template %s: %s", + CONF_AVAILABILITY_TEMPLATE, + self._name, + ex, + ) diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 9e3c535f136d3c..da0e8e59ededb8 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -3,7 +3,7 @@ import pytest from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_UNKNOWN +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN, STATE_UNAVAILABLE from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, STATE_CLEANING, @@ -210,6 +210,68 @@ async def test_invalid_templates(hass, calls): _verify(hass, STATE_UNKNOWN, None) +async def test_available_template_with_entities(hass, calls): + """Test availability templates with values from other entities.""" + + assert await setup.async_setup_component( + hass, + "vacuum", + { + "vacuum": { + "platform": "template", + "vacuums": { + "test_template_vacuum": { + "availability_template": "{{ is_state('availability_state.state', 'on') }}", + "start": {"service": "script.vacuum_start"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get("vacuum.test_template_vacuum").state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get("vacuum.test_template_vacuum").state == STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + assert await setup.async_setup_component( + hass, + "vacuum", + { + "vacuum": { + "platform": "template", + "vacuums": { + "test_template_vacuum": { + "availability_template": "{{ x - 12 }}", + "start": {"service": "script.vacuum_start"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("vacuum.test_template_vacuum") != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text + + # End of template tests # From 61a7d8e3d26c19288f9b55af2bcf63fe907c2906 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 28 Sep 2019 22:34:14 +0200 Subject: [PATCH 208/296] Add create, remove of devices for HomematicIP_Cloud (#27030) --- .../components/homematicip_cloud/device.py | 47 +++++++++++++++++++ .../components/homematicip_cloud/hap.py | 14 ++++++ .../homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 05853d4b260bca..1273278189d89f 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -6,6 +6,8 @@ from homematicip.aio.home import AsyncHome from homeassistant.components import homematicip_cloud +from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -51,6 +53,8 @@ def __init__(self, home: AsyncHome, device, post: Optional[str] = None) -> None: self._home = home self._device = device self.post = post + # Marker showing that the HmIP device hase been removed. + self.hmip_device_removed = False _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @property @@ -74,7 +78,9 @@ def device_info(self): async def async_added_to_hass(self): """Register callbacks.""" self._device.on_update(self._async_device_changed) + self._device.on_remove(self._async_device_removed) + @callback def _async_device_changed(self, *args, **kwargs): """Handle device state changes.""" # Don't update disabled entities @@ -88,6 +94,47 @@ def _async_device_changed(self, *args, **kwargs): self._device.modelType, ) + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + + # Only go further if the device/entity should be removed from registries + # due to a removal of the HmIP device. + if self.hmip_device_removed: + await self.async_remove_from_registries() + + async def async_remove_from_registries(self) -> None: + """Remove entity/device from registry.""" + + # Remove callback from device. + self._device.remove_callback(self._async_device_changed) + self._device.remove_callback(self._async_device_removed) + + if not self.registry_entry: + return + + device_id = self.registry_entry.device_id + if device_id: + # Remove from device registry. + device_registry = await dr.async_get_registry(self.hass) + if device_id in device_registry.devices: + # This will also remove associated entities from entity registry. + device_registry.async_remove_device(device_id) + else: + # Remove from entity registry. + # Only relevant for entities that do not belong to a device. + entity_id = self.registry_entry.entity_id + if entity_id: + entity_registry = await er.async_get_registry(self.hass) + if entity_id in entity_registry.entities: + entity_registry.async_remove(entity_id) + + @callback + def _async_device_removed(self, *args, **kwargs): + """Handle hmip device removal.""" + # Set marker showing that the HmIP device hase been removed. + self.hmip_device_removed = True + self.hass.async_create_task(self.async_remove()) + @property def name(self) -> str: """Return the name of the generic device.""" diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 23973efb07b1c7..abba183d339b8e 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -5,6 +5,7 @@ from homematicip.aio.auth import AsyncAuth from homematicip.aio.home import AsyncHome from homematicip.base.base_connection import HmipConnectionError +from homematicip.base.enums import EventType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -137,6 +138,18 @@ def async_update(self, *args, **kwargs): self.home.update_home_only(args[0]) + @callback + def async_create_entity(self, *args, **kwargs): + """Create a device or a group.""" + is_device = EventType(kwargs["event_type"]) == EventType.DEVICE_ADDED + self.hass.async_create_task(self.async_create_entity_lazy(is_device)) + + async def async_create_entity_lazy(self, is_device=True): + """Delay entity creation to allow the user to enter a device name.""" + if is_device: + await asyncio.sleep(30) + await self.hass.config_entries.async_reload(self.config_entry.entry_id) + async def get_state(self): """Update HMIP state and tell Home Assistant.""" await self.home.get_current_state() @@ -225,6 +238,7 @@ async def get_hap( except HmipConnectionError: raise HmipcConnectionError home.on_update(self.async_update) + home.on_create(self.async_create_entity) hass.loop.create_task(self.async_connect()) return home diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index b83358822b9f3f..2075f88ded25ab 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/homematicip_cloud", "requirements": [ - "homematicip==0.10.11" + "homematicip==0.10.12" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ab99704f9653cb..9b814ef8eddf12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -649,7 +649,7 @@ homeassistant-pyozw==0.1.4 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.11 +homematicip==0.10.12 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ac46e96cd4d78..9599d24b20a1b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -188,7 +188,7 @@ home-assistant-frontend==20190919.1 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.11 +homematicip==0.10.12 # homeassistant.components.google # homeassistant.components.remember_the_milk From 560ac3df3a400f5b413fa4028c1bcfeefac9d9c9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 29 Sep 2019 00:32:13 +0000 Subject: [PATCH 209/296] [ci skip] Translation update --- .../components/adguard/.translations/hu.json | 13 +++ .../components/axis/.translations/hu.json | 3 +- .../binary_sensor/.translations/hu.json | 92 +++++++++++++++++++ .../binary_sensor/.translations/it.json | 92 +++++++++++++++++++ .../binary_sensor/.translations/pl.json | 73 ++++++++++++++- .../components/deconz/.translations/hu.json | 5 + .../components/deconz/.translations/pl.json | 22 ++--- .../components/ecobee/.translations/it.json | 25 +++++ .../components/light/.translations/pl.json | 8 +- .../components/met/.translations/hu.json | 13 ++- .../components/plex/.translations/it.json | 23 +++++ .../components/plex/.translations/pl.json | 12 +++ .../simplisafe/.translations/pl.json | 2 +- .../components/switch/.translations/hu.json | 19 ++++ .../components/switch/.translations/pl.json | 12 +-- .../transmission/.translations/it.json | 40 ++++++++ .../components/zha/.translations/it.json | 47 ++++++++++ .../components/zha/.translations/pl.json | 47 ++++++++++ 18 files changed, 521 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/adguard/.translations/hu.json create mode 100644 homeassistant/components/binary_sensor/.translations/hu.json create mode 100644 homeassistant/components/binary_sensor/.translations/it.json create mode 100644 homeassistant/components/ecobee/.translations/it.json create mode 100644 homeassistant/components/switch/.translations/hu.json create mode 100644 homeassistant/components/transmission/.translations/it.json diff --git a/homeassistant/components/adguard/.translations/hu.json b/homeassistant/components/adguard/.translations/hu.json new file mode 100644 index 00000000000000..34b601027c2211 --- /dev/null +++ b/homeassistant/components/adguard/.translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/hu.json b/homeassistant/components/axis/.translations/hu.json index b0c8051e69f9fa..41dd3c00d2b32d 100644 --- a/homeassistant/components/axis/.translations/hu.json +++ b/homeassistant/components/axis/.translations/hu.json @@ -14,6 +14,7 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } - } + }, + "title": "Axis eszk\u00f6z" } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/hu.json b/homeassistant/components/binary_sensor/.translations/hu.json new file mode 100644 index 00000000000000..e53d918f98d28e --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/hu.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony", + "is_cold": "{entity_name} hideg", + "is_connected": "{entity_name} csatlakoztatva van", + "is_gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", + "is_hot": "{entity_name} forr\u00f3", + "is_light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", + "is_locked": "{entity_name} z\u00e1rva van", + "is_moist": "{entity_name} nedves", + "is_motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel", + "is_moving": "{entity_name} mozog", + "is_no_gas": "{entity_name} nem \u00e9rz\u00e9kel g\u00e1zt", + "is_no_light": "{entity_name} nem \u00e9rz\u00e9kel f\u00e9nyt", + "is_no_motion": "{entity_name} nem \u00e9rz\u00e9kel mozg\u00e1st", + "is_no_problem": "{entity_name} nem \u00e9szlel probl\u00e9m\u00e1t", + "is_no_smoke": "{entity_name} nem \u00e9rz\u00e9kel f\u00fcst\u00f6t", + "is_no_sound": "{entity_name} nem \u00e9rz\u00e9kel hangot", + "is_no_vibration": "{entity_name} nem \u00e9rz\u00e9kel rezg\u00e9st", + "is_not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", + "is_not_cold": "{entity_name} nem hideg", + "is_not_connected": "{entity_name} le van csatlakoztatva", + "is_not_hot": "{entity_name} nem forr\u00f3", + "is_not_locked": "{entity_name} nyitva van", + "is_not_moist": "{entity_name} sz\u00e1raz", + "is_not_moving": "{entity_name} nem mozog", + "is_not_occupied": "{entity_name} nem foglalt", + "is_not_open": "{entity_name} z\u00e1rva van", + "is_not_plugged_in": "{entity_name} nincs csatlakoztatva", + "is_not_powered": "{entity_name} nincs fesz\u00fcts\u00e9g alatt", + "is_not_present": "{entity_name} nincs jelen", + "is_not_unsafe": "{entity_name} biztons\u00e1gos", + "is_occupied": "{entity_name} foglalt", + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva", + "is_open": "{entity_name} nyitva van", + "is_plugged_in": "{entity_name} csatlakoztatva van", + "is_powered": "{entity_name} fesz\u00fclts\u00e9g alatt van", + "is_present": "{entity_name} jelen van", + "is_problem": "{entity_name} probl\u00e9m\u00e1t \u00e9szlel", + "is_smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", + "is_sound": "{entity_name} hangot \u00e9rz\u00e9kel", + "is_unsafe": "{entity_name} nem biztons\u00e1gos", + "is_vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" + }, + "trigger_type": { + "bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony", + "closed": "{entity_name} be lett z\u00e1rva", + "cold": "{entity_name} hideg lett", + "connected": "{entity_name} csatlakozott", + "gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", + "hot": "{entity_name} felforr\u00f3sodott", + "light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", + "locked": "{entity_name} be lett z\u00e1rva", + "moist\u00a7": "{entity_name} nedves lett", + "motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel", + "moving": "{entity_name} mozog", + "no_gas": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel g\u00e1zt", + "no_light": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel f\u00e9nyt", + "no_motion": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel mozg\u00e1st", + "no_problem": "{entity_name} m\u00e1r nem \u00e9szlel probl\u00e9m\u00e1t", + "no_smoke": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel f\u00fcst\u00f6t", + "no_sound": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel hangot", + "no_vibration": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel rezg\u00e9st", + "not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", + "not_cold": "{entity_name} m\u00e1r nem hideg", + "not_connected": "{entity_name} lecsatlakozott", + "not_hot": "{entity_name} m\u00e1r nem forr\u00f3", + "not_locked": "{entity_name} ki lett nyitva", + "not_moist": "{entity_name} sz\u00e1raz lett", + "not_moving": "{entity_name} m\u00e1r nem mozog", + "not_occupied": "{entity_name} m\u00e1r nem foglalt", + "not_plugged_in": "{entity_name} m\u00e1r nincs csatlakoztatva", + "not_powered": "{entity_name} m\u00e1r nincs fesz\u00fcts\u00e9g alatt", + "not_present": "{entity_name} m\u00e1r nincs jelen", + "not_unsafe": "{entity_name} biztons\u00e1gos lett", + "occupied": "{entity_name} foglalt lett", + "opened": "{entity_name} ki lett nyitva", + "plugged_in": "{entity_name} csatlakoztatva lett", + "powered": "{entity_name} m\u00e1r fesz\u00fclts\u00e9g alatt van", + "present": "{entity_name} m\u00e1r jelen van", + "problem": "{entity_name} probl\u00e9m\u00e1t \u00e9szlel", + "smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", + "sound": "{entity_name} hangot \u00e9rz\u00e9kel", + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva", + "unsafe": "{entity_name} m\u00e1r nem biztons\u00e1gos", + "vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/it.json b/homeassistant/components/binary_sensor/.translations/it.json new file mode 100644 index 00000000000000..0583a4d4f74c09 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/it.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} la batteria \u00e8 scarica", + "is_cold": "{entity_name} \u00e8 freddo", + "is_connected": "{entity_name} \u00e8 collegato", + "is_gas": "{entity_name} sta rilevando il gas", + "is_hot": "{entity_name} \u00e8 caldo", + "is_light": "{entity_name} sta rilevando la luce", + "is_locked": "{entity_name} \u00e8 bloccato", + "is_moist": "{entity_name} \u00e8 umido", + "is_motion": "{entity_name} sta rilevando il movimento", + "is_moving": "{entity_name} si sta muovendo", + "is_no_gas": "{entity_name} non sta rilevando il gas", + "is_no_light": "{entity_name} non sta rilevando la luce", + "is_no_motion": "{entity_name} non sta rilevando il movimento", + "is_no_problem": "{entity_name} non sta rilevando un problema", + "is_no_smoke": "{entity_name} non sta rilevando il fumo", + "is_no_sound": "{entity_name} non sta rilevando il suono", + "is_no_vibration": "{entity_name} non sta rilevando la vibrazione", + "is_not_bat_low": "{entity_name} la batteria \u00e8 normale", + "is_not_cold": "{entity_name} non \u00e8 freddo", + "is_not_connected": "{entity_name} \u00e8 disconnesso", + "is_not_hot": "{entity_name} non \u00e8 caldo", + "is_not_locked": "{entity_name} \u00e8 sbloccato", + "is_not_moist": "{entity_name} \u00e8 asciutto", + "is_not_moving": "{entity_name} non si sta muovendo", + "is_not_occupied": "{entity_name} non \u00e8 occupato", + "is_not_open": "{entity_name} \u00e8 chiuso", + "is_not_plugged_in": "{entity_name} \u00e8 collegato", + "is_not_powered": "{entity_name} non \u00e8 alimentato", + "is_not_present": "{entity_name} non \u00e8 presente", + "is_not_unsafe": "{entity_name} \u00e8 sicuro", + "is_occupied": "{entity_name} \u00e8 occupato", + "is_off": "{entity_name} \u00e8 spento", + "is_on": "{entity_name} \u00e8 acceso", + "is_open": "{entity_name} \u00e8 aperto", + "is_plugged_in": "{entity_name} \u00e8 collegato", + "is_powered": "{entity_name} \u00e8 alimentato", + "is_present": "{entity_name} \u00e8 presente", + "is_problem": "{entity_name} sta rilevando un problema", + "is_smoke": "{entity_name} sta rilevando il fumo", + "is_sound": "{entity_name} sta rilevando il suono", + "is_unsafe": "{entity_name} non \u00e8 sicuro", + "is_vibration": "{entity_name} sta rilevando la vibrazione" + }, + "trigger_type": { + "bat_low": "{entity_name} batteria scarica", + "closed": "{entity_name} \u00e8 chiuso", + "cold": "{entity_name} \u00e8 diventato freddo", + "connected": "{entity_name} connesso", + "gas": "{entity_name} ha iniziato a rilevare il gas", + "hot": "{entity_name} \u00e8 diventato caldo", + "light": "{entity_name} ha iniziato a rilevare la luce", + "locked": "{entity_name} bloccato", + "moist\u00a7": "{entity_name} \u00e8 diventato umido", + "motion": "{entity_name} ha iniziato a rilevare il movimento", + "moving": "{entity_name} ha iniziato a muoversi", + "no_gas": "{entity_name} ha smesso la rilevazione di gas", + "no_light": "{entity_name} smesso il rilevamento di luce", + "no_motion": "{nome_entit\u00e0} ha smesso di rilevare il movimento", + "no_problem": "{nome_entit\u00e0} ha smesso di rilevare un problema", + "no_smoke": "{entity_name} ha smesso la rilevazione di fumo", + "no_sound": "{nome_entit\u00e0} ha smesso di rilevare il suono", + "no_vibration": "{nome_entit\u00e0} ha smesso di rilevare le vibrazioni", + "not_bat_low": "{entity_name} batteria normale", + "not_cold": "{entity_name} non \u00e8 diventato freddo", + "not_connected": "{entity_name} \u00e8 disconnesso", + "not_hot": "{entity_name} non \u00e8 diventato caldo", + "not_locked": "{entity_name} \u00e8 sbloccato", + "not_moist": "{entity_name} \u00e8 diventato asciutto", + "not_moving": "{entity_name} ha smesso di muoversi", + "not_occupied": "{entity_name} non \u00e8 occupato", + "not_plugged_in": "{entity_name} \u00e8 scollegato", + "not_powered": "{entity_name} non \u00e8 alimentato", + "not_present": "{entity_name} non \u00e8 presente", + "not_unsafe": "{entity_name} \u00e8 diventato sicuro", + "occupied": "{entity_name} \u00e8 diventato occupato", + "opened": "{entity_name} \u00e8 aperto", + "plugged_in": "{entity_name} \u00e8 collegato", + "powered": "{entity_name} \u00e8 alimentato", + "present": "{entity_name} \u00e8 presente", + "problem": "{entity_name} ha iniziato a rilevare un problema", + "smoke": "{entity_name} ha iniziato la rilevazione di fumo", + "sound": "{entity_name} ha iniziato il rilevamento del suono", + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato", + "unsafe": "{entity_name} diventato non sicuro", + "vibration": "{entity_name} iniziato a rilevare le vibrazioni" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json index 7862644c727826..059800a116fe9d 100644 --- a/homeassistant/components/binary_sensor/.translations/pl.json +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -3,11 +3,11 @@ "condition_type": { "is_bat_low": "Bateria {entity_name} jest roz\u0142adowana", "is_cold": "{entity_name} wykrywa zimno", - "is_connected": "{entity_name} jest po\u0142\u0105czone", + "is_connected": "{entity_name} jest po\u0142\u0105czony", "is_gas": "{entity_name} wykrywa gaz", "is_hot": "{entity_name} wykrywa gor\u0105co", "is_light": "{entity_name} wykrywa \u015bwiat\u0142o", - "is_locked": "{entity_name} jest zamkni\u0119te", + "is_locked": "{entity_name} jest zamkni\u0119ty", "is_moist": "{entity_name} wykrywa wilgo\u0107", "is_motion": "{entity_name} wykrywa ruch", "is_moving": "{entity_name} porusza si\u0119", @@ -18,8 +18,75 @@ "is_no_smoke": "{entity_name} nie wykrywa dymu", "is_no_sound": "{entity_name} nie wykrywa d\u017awi\u0119ku", "is_no_vibration": "{entity_name} nie wykrywa wibracji", + "is_not_bat_low": "Bateria {entity_name} nie jest roz\u0142adowana", + "is_not_cold": "{entity_name} nie wykrywa zimna", + "is_not_connected": "{entity_name} jest roz\u0142\u0105czony", + "is_not_hot": "{entity_name} nie wykrywa gor\u0105ca", + "is_not_locked": "{entity_name} jest otwarty", + "is_not_moist": "{entity_name} nie wykrywa wilgoci", + "is_not_moving": "{entity_name} nie porusza si\u0119", + "is_not_occupied": "{entity_name} nie jest zaj\u0119ty", + "is_not_open": "{entity_name} jest zamkni\u0119ty", + "is_not_plugged_in": "{entity_name} jest od\u0142\u0105czony", + "is_not_powered": "{entity_name} nie jest zasilany", + "is_not_present": "{entity_name} nie wykrywa obecno\u015bci", + "is_not_unsafe": "{entity_name} raportuje bezpiecze\u0144stwo", + "is_occupied": "{entity_name} jest zaj\u0119ty", "is_off": "{entity_name} jest wy\u0142\u0105czone", - "is_on": "{entity_name} jest w\u0142\u0105czone" + "is_on": "{entity_name} jest w\u0142\u0105czone", + "is_open": "{entity_name} jest otwarty", + "is_plugged_in": "{entity_name} jest pod\u0142\u0105czony", + "is_powered": "{entity_name} jest zasilany", + "is_present": "{entity_name} wykrywa obecno\u015b\u0107", + "is_problem": "{entity_name} wykrywa problem", + "is_smoke": "{entity_name} wykrywa dym", + "is_sound": "{entity_name} wykrywa d\u017awi\u0119k", + "is_unsafe": "{entity_name} raportuje niebezpiecze\u0144stwo", + "is_vibration": "{entity_name} wykrywa wibracje" + }, + "trigger_type": { + "bat_low": "Bateria {entity_name} staje si\u0119 roz\u0142adowanie", + "closed": "Zamkni\u0119cie {entity_name}", + "cold": "Wykrycie zimna przez {entity_name}", + "connected": "Pod\u0142\u0105czenie {entity_name}", + "gas": "Wykrycie gazu przez {entity_name}", + "hot": "Wykrycie gor\u0105ca przez {entity_name}", + "light": "Wykrycie \u015bwiat\u0142a przez {entity_name}", + "locked": "Zamkni\u0119cie {entity_name}", + "moist\u00a7": "Wykrycie wilgoci przez {entity_name}", + "motion": "Wykrycie ruchu przez {entity_name}", + "moving": "{entity_name} zacz\u0105\u0142 si\u0119 porusza\u0107", + "no_gas": "Wykrycie braku gazu przez {entity_name}", + "no_light": "Wykrycie braku \u015bwiat\u0142a przez {entity_name}", + "no_motion": "Wykrycie braku ruchu przez {entity_name}", + "no_problem": "Wykrycie braku problemu przez {entity_name}", + "no_smoke": "Wykrycie braku dymu przez {entity_name}", + "no_sound": "Wykrycie braku d\u017awi\u0119ku przez {entity_name}", + "no_vibration": "Wykrycie braku wibracji przez {entity_name}", + "not_bat_low": "Bateria {entity_name} staje si\u0119 na\u0142adowana", + "not_cold": "Wykrycie braku zimna przez {entity_name}", + "not_connected": "Roz\u0142\u0105czenie {entity_name}", + "not_hot": "Wykrycie braku gor\u0105ca przez {entity_name}", + "not_locked": "Otwarcie {entity_name}", + "not_moist": "Wykrycie braku wilgoci przez {entity_name}", + "not_moving": "{entity_name} przesta\u0142 si\u0119 porusza\u0107", + "not_occupied": "{entity_name} sta\u0142 si\u0119 niezaj\u0119ty", + "not_plugged_in": "Od\u0142\u0105czenie {entity_name}", + "not_powered": "Brak zasilania dla {entity_name}", + "not_present": "Wykrycie braku obecno\u015bci przez {entity_name}", + "not_unsafe": "Raportowanie bezpiecze\u0144stwa przez {entity_name}", + "occupied": "{entity_name} sta\u0142 si\u0119 zaj\u0119ty", + "opened": "Otwarcie {entity_name}", + "plugged_in": "Pod\u0142\u0105czenie {entity_name}", + "powered": "Zasilenie {entity_name}", + "present": "Wykrycie obecno\u015bci przez {entity_name}", + "problem": "Wykrycie problemu przez {entity_name}", + "smoke": "Wykrycie dymu przez {entity_name}", + "sound": "Wykrycie d\u017awi\u0119ku przez {entity_name}", + "turned_off": "Wy\u0142\u0105czenie {entity_name}", + "turned_on": "W\u0142\u0105czenie {entity_name}", + "unsafe": "Raportowanie niebezpiecze\u0144stwa przez {entity_name}", + "vibration": "Wykrycie wibracji przez {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index 5bf8db4684190f..9e8109107436df 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -29,5 +29,10 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "device_automation": { + "trigger_subtype": { + "close": "Bez\u00e1r\u00e1s" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 70c33cf3c02f4f..d92f318f61fda7 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -44,18 +44,18 @@ "device_automation": { "trigger_subtype": { "both_buttons": "Oba przyciski", - "button_1": "Pierwszy przycisk", - "button_2": "Drugi przycisk", - "button_3": "Trzeci przycisk", - "button_4": "Czwarty przycisk", - "close": "Zamknij", + "button_1": "pierwszy przycisk", + "button_2": "drugi przycisk", + "button_3": "trzeci przycisk", + "button_4": "czwarty przycisk", + "close": "Zamkni\u0119cie", "dim_down": "Przyciemnienie", "dim_up": "Przyciemnienie", - "left": "Lewo", - "open": "Otw\u00f3rz", - "right": "Prawo", - "turn_off": "Wy\u0142\u0105cz", - "turn_on": "W\u0142\u0105cz" + "left": "w lewo", + "open": "Otwarcie", + "right": "w prawo", + "turn_off": "Wy\u0142\u0105czenie", + "turn_on": "W\u0142\u0105czenie" }, "trigger_type": { "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", @@ -67,7 +67,7 @@ "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty", "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony", "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", - "remote_gyro_activated": "Urz\u0105dzenie potrz\u0105\u015bni\u0119te" + "remote_gyro_activated": "Potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" } }, "options": { diff --git a/homeassistant/components/ecobee/.translations/it.json b/homeassistant/components/ecobee/.translations/it.json new file mode 100644 index 00000000000000..2ecb587f19e13a --- /dev/null +++ b/homeassistant/components/ecobee/.translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Questa integrazione supporta attualmente una sola istanza ecobee." + }, + "error": { + "pin_request_failed": "Errore durante la richiesta del PIN da ecobee; verificare che la chiave API sia corretta.", + "token_request_failed": "Errore durante la richiesta di token da ecobee; per favore riprova." + }, + "step": { + "authorize": { + "description": "Autorizza questa app su https://www.ecobee.com/consumerportal/index.html con il codice PIN: \n\n {pin} \n \n Quindi, premi Invia.", + "title": "Autorizza l'app su ecobee.com" + }, + "user": { + "data": { + "api_key": "API Key" + }, + "description": "Inserisci la chiave API ottenuta da ecobee.com.", + "title": "chiave API ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 17c81c471f5018..4b649744ed977c 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -6,12 +6,12 @@ "turn_on": "W\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "{entity_name} jest wy\u0142\u0105czone", - "is_on": "{entity_name} jest w\u0142\u0105czony." + "is_off": "{entity_name} jest wy\u0142\u0105czony", + "is_on": "{entity_name} jest w\u0142\u0105czony" }, "trigger_type": { - "turned_off": "{entity_name} wy\u0142\u0105czone", - "turned_on": "{entity_name} w\u0142\u0105czone" + "turned_off": "Wy\u0142\u0105czenie {entity_name}", + "turned_on": "W\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/met/.translations/hu.json b/homeassistant/components/met/.translations/hu.json index 3b34d8f6354d6a..dcbc40b4c71729 100644 --- a/homeassistant/components/met/.translations/hu.json +++ b/homeassistant/components/met/.translations/hu.json @@ -1,9 +1,20 @@ { "config": { + "error": { + "name_exists": "A hely m\u00e1r l\u00e9tezik" + }, "step": { "user": { + "data": { + "elevation": "Magass\u00e1g", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + }, + "description": "Meteorol\u00f3giai int\u00e9zet", "title": "Elhelyezked\u00e9s" } - } + }, + "title": "Met.no" } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index 2e77b4ba9768b5..3c28f1d25f9c6b 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "Autorizzazione non riuscita", "no_servers": "Nessun server collegato all'account", + "no_token": "Fornire un token o selezionare la configurazione manuale", "not_found": "Server Plex non trovato" }, "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Porta", + "ssl": "Usa SSL", + "token": "Token (se richiesto)", + "verify_ssl": "Verificare il certificato SSL" + }, + "title": "Server Plex" + }, "select_server": { "data": { "server": "Server" @@ -22,6 +33,7 @@ }, "user": { "data": { + "manual_setup": "Configurazione manuale", "token": "Token Plex" }, "description": "Immettere un token Plex per la configurazione automatica.", @@ -29,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostra tutti i controlli", + "use_episode_art": "Usa la grafica dell'episodio" + }, + "description": "Opzioni per i lettori multimediali Plex" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index ea1db4ec2f3243..ce9d2e1e88d73c 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "Autoryzacja nie powiod\u0142a si\u0119", "no_servers": "Brak serwer\u00f3w po\u0142\u0105czonych z kontem", + "no_token": "Wprowad\u017a token lub wybierz konfiguracj\u0119 r\u0119czn\u0105", "not_found": "Nie znaleziono serwera Plex" }, "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "U\u017cyj SSL", + "token": "Token (je\u015bli wymagany)", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "title": "Serwer Plex" + }, "select_server": { "data": { "server": "Serwer" @@ -22,6 +33,7 @@ }, "user": { "data": { + "manual_setup": "Konfiguracja r\u0119czna", "token": "Token Plex" }, "description": "Wprowad\u017a token Plex do automatycznej konfiguracji.", diff --git a/homeassistant/components/simplisafe/.translations/pl.json b/homeassistant/components/simplisafe/.translations/pl.json index c4d616600f56a7..ad8a15d06b7455 100644 --- a/homeassistant/components/simplisafe/.translations/pl.json +++ b/homeassistant/components/simplisafe/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "Konto jest ju\u017c zarejestrowane", - "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { "user": { diff --git a/homeassistant/components/switch/.translations/hu.json b/homeassistant/components/switch/.translations/hu.json new file mode 100644 index 00000000000000..c3ea3190694baf --- /dev/null +++ b/homeassistant/components/switch/.translations/hu.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} be/kikapcsol\u00e1sa", + "turn_off": "{entity_name} kikapcsol\u00e1sa", + "turn_on": "{entity_name} bekapcsol\u00e1sa" + }, + "condition_type": { + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva", + "turn_off": "{entity_name} ki lett kapcsolva", + "turn_on": "{entity_name} be lett kapcsolva" + }, + "trigger_type": { + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index 31187aaa1b7cc4..201a77a76a5d09 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -6,14 +6,14 @@ "turn_on": "W\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "{entity_name} jest wy\u0142\u0105czone", - "is_on": "{entity_name} jest w\u0142\u0105czone", - "turn_off": "{entity_name} wy\u0142\u0105czone", - "turn_on": "{entity_name} w\u0142\u0105czone" + "is_off": "{entity_name} jest wy\u0142\u0105czony", + "is_on": "{entity_name} jest w\u0142\u0105czony", + "turn_off": "{entity_name} wy\u0142\u0105czony", + "turn_on": "{entity_name} w\u0142\u0105czony" }, "trigger_type": { - "turned_off": "{entity_name} wy\u0142\u0105czone", - "turned_on": "{entity_name} w\u0142\u0105czone" + "turned_off": "Wy\u0142\u0105czenie {entity_name}", + "turned_on": "W\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/it.json b/homeassistant/components/transmission/.translations/it.json new file mode 100644 index 00000000000000..17a03b6dba1745 --- /dev/null +++ b/homeassistant/components/transmission/.translations/it.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\u00c8 necessaria solo una singola istanza." + }, + "error": { + "cannot_connect": "Impossibile connettersi all'host", + "wrong_credentials": "Nome utente o password non validi" + }, + "step": { + "options": { + "data": { + "scan_interval": "Frequenza di aggiornamento" + }, + "title": "Configura opzioni" + }, + "user": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "title": "Configura client di Trasmissione" + } + }, + "title": "Trasmissione" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequenza di aggiornamento" + }, + "description": "Configurare le opzioni per Trasmissione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/it.json b/homeassistant/components/zha/.translations/it.json index e4b87c9d7b6dfb..bb05977fd098b9 100644 --- a/homeassistant/components/zha/.translations/it.json +++ b/homeassistant/components/zha/.translations/it.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Strillare", + "warn": "Avvertire" + }, + "trigger_subtype": { + "both_buttons": "Entrambi i pulsanti", + "button_1": "Primo pulsante", + "button_2": "Secondo pulsante", + "button_3": "Terzo pulsante", + "button_4": "Quarto pulsante", + "button_5": "Quinto pulsante", + "button_6": "Sesto pulsante", + "close": "Chiudere", + "dim_down": "Diminuire luminosit\u00e0", + "dim_up": "Aumentare luminosit\u00e0", + "face_1": "con faccia 1 attivata", + "face_2": "con faccia 2 attivata", + "face_3": "con faccia 3 attivata", + "face_4": "con faccia 4 attivata", + "face_5": "con faccia 5 attivata", + "face_6": "con faccia 6 attivata", + "face_any": "Con una o pi\u00f9 facce specificate attivate", + "left": "Sinistra", + "open": "Aperto", + "right": "Destra", + "turn_off": "Spento", + "turn_on": "Acceso" + }, + "trigger_type": { + "device_dropped": "Dispositivo caduto", + "device_flipped": "Dispositivo capovolto \" {subtype} \"", + "device_knocked": "Dispositivo bussato \" {subtype} \"", + "device_rotated": "Dispositivo ruotato \" {subtype} \"", + "device_shaken": "Dispositivo in vibrazione", + "device_slid": "Dispositivo scivolato \"{sottotipo}\"", + "device_tilted": "Dispositivo inclinato", + "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", + "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", + "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", + "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", + "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", + "remote_button_short_press": "Pulsante \"{subtype}\" premuto", + "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", + "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pl.json b/homeassistant/components/zha/.translations/pl.json index 93867c0c84f051..76f1c58fe7ccc8 100644 --- a/homeassistant/components/zha/.translations/pl.json +++ b/homeassistant/components/zha/.translations/pl.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Skrzek", + "warn": "Ostrze\u017cenie" + }, + "trigger_subtype": { + "both_buttons": "Oba przyciski", + "button_1": "Pierwszy przycisk", + "button_2": "Drugi przycisk", + "button_3": "Trzeci przycisk", + "button_4": "Czwarty przycisk", + "button_5": "Pi\u0105ty przycisk", + "button_6": "Sz\u00f3sty przycisk", + "close": "Zamkni\u0119cie", + "dim_down": "\u015aciemnianie", + "dim_up": "Rozja\u015bnienie", + "face_1": "z aktywowan\u0105 twarz\u0105 1", + "face_2": "z aktywowan\u0105 twarz\u0105 2", + "face_3": "z aktywowan\u0105 twarz\u0105 3", + "face_4": "z aktywowan\u0105 twarz\u0105 4", + "face_5": "z aktywowan\u0105 twarz\u0105 5", + "face_6": "z aktywowan\u0105 twarz\u0105 6", + "face_any": "z dowoln\u0105 twarz\u0105 aktywowan\u0105", + "left": "w lewo", + "open": "Otwarcie", + "right": "w prawo", + "turn_off": "Wy\u0142\u0105czenie", + "turn_on": "W\u0142\u0105czenie" + }, + "trigger_type": { + "device_dropped": "Upadek urz\u0105dzenia", + "device_flipped": "Odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_knocked": "Pukni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_rotated": "Obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_shaken": "Potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", + "device_slid": "Przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_tilted": "Przechylenie urz\u0105dzenia", + "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "Przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "Przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty", + "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony", + "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty" + } } } \ No newline at end of file From f464a7808878093351986c87109724539163ec4c Mon Sep 17 00:00:00 2001 From: david81 Date: Sat, 28 Sep 2019 23:36:35 -0400 Subject: [PATCH 210/296] Add venstar support for hvac action (#26956) * Added support for current fan state and hvac action * Corrected handling of fan_mode --- homeassistant/components/venstar/climate.py | 29 ++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 7e1ae1ecd60b09..7be31d56c08275 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -11,14 +11,20 @@ HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, SUPPORT_FAN_MODE, + FAN_ON, + FAN_AUTO, SUPPORT_TARGET_HUMIDITY, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, PRESET_AWAY, PRESET_NONE, SUPPORT_TARGET_TEMPERATURE_RANGE, - HVAC_MODE_OFF, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -156,7 +162,7 @@ def current_humidity(self): @property def hvac_mode(self): - """Return current operation ie. heat, cool, idle.""" + """Return current operation mode ie. heat, cool, auto.""" if self._client.mode == self._client.MODE_HEAT: return HVAC_MODE_HEAT if self._client.mode == self._client.MODE_COOL: @@ -165,12 +171,23 @@ def hvac_mode(self): return HVAC_MODE_AUTO return HVAC_MODE_OFF + @property + def hvac_action(self): + """Return current operation mode ie. heat, cool, auto.""" + if self._client.state == self._client.STATE_IDLE: + return CURRENT_HVAC_IDLE + if self._client.state == self._client.STATE_HEATING: + return CURRENT_HVAC_HEAT + if self._client.state == self._client.STATE_COOLING: + return CURRENT_HVAC_COOL + return CURRENT_HVAC_OFF + @property def fan_mode(self): - """Return the fan setting.""" - if self._client.fan == self._client.FAN_AUTO: - return HVAC_MODE_AUTO - return STATE_ON + """Return the current fan mode.""" + if self._client.fan == self._client.FAN_ON: + return FAN_ON + return FAN_AUTO @property def device_state_attributes(self): From 2ebc1901abb8b4e1fadcef44ae9d5509f9b8052a Mon Sep 17 00:00:00 2001 From: Khole Date: Sun, 29 Sep 2019 10:38:43 +0100 Subject: [PATCH 211/296] Change hive hotwater to hot_water + bug fix (#27038) * Updated hotwater to hot_water + bug fix * Updated version seperating dependancy --- homeassistant/components/hive/__init__.py | 12 ++++++------ homeassistant/components/hive/manifest.json | 2 +- homeassistant/components/hive/services.yaml | 2 +- requirements_all.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index c11eb18accad98..3301097bab79b2 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -23,7 +23,7 @@ DOMAIN = "hive" DATA_HIVE = "data_hive" SERVICES = ["Heating", "HotWater"] -SERVICE_BOOST_HOTWATER = "boost_hotwater" +SERVICE_BOOST_HOT_WATER = "boost_hot_water" SERVICE_BOOST_HEATING = "boost_heating" ATTR_TIME_PERIOD = "time_period" ATTR_MODE = "on_off" @@ -59,7 +59,7 @@ } ) -BOOST_HOTWATER_SCHEMA = vol.Schema( +BOOST_HOT_WATER_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_id, vol.Optional(ATTR_TIME_PERIOD, default="00:30:00"): vol.All( @@ -100,7 +100,7 @@ def heating_boost(service): session.heating.turn_boost_on(node_id, minutes, temperature) - def hotwater_boost(service): + def hot_water_boost(service): """Handle the service call.""" node_id = HiveSession.entity_lookup.get(service.data[ATTR_ENTITY_ID]) if not node_id: @@ -151,9 +151,9 @@ def hotwater_boost(service): if ha_type == "water_heater": hass.services.register( DOMAIN, - SERVICE_BOOST_HEATING, - hotwater_boost, - schema=BOOST_HOTWATER_SCHEMA, + SERVICE_BOOST_HOT_WATER, + hot_water_boost, + schema=BOOST_HOT_WATER_SCHEMA, ) return True diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 2e7c4f4f179dee..d9fae3fe54b936 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "documentation": "https://www.home-assistant.io/components/hive", "requirements": [ - "pyhiveapi==0.2.19" + "pyhiveapi==0.2.19.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml index 27d7acfc83bab0..6513d76ca89981 100644 --- a/homeassistant/components/hive/services.yaml +++ b/homeassistant/components/hive/services.yaml @@ -14,7 +14,7 @@ boost_heating: description: Set the target temperature for the boost period., example: "20.5", } -boost_hotwater: +boost_hot_water: description: "Set the boost mode ON or OFF defining the period of time for the boost." fields: diff --git a/requirements_all.txt b/requirements_all.txt index 9b814ef8eddf12..79313b48e61ed2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1223,7 +1223,7 @@ pyheos==0.6.0 pyhik==0.2.3 # homeassistant.components.hive -pyhiveapi==0.2.19 +pyhiveapi==0.2.19.2 # homeassistant.components.homematic pyhomematic==0.1.60 From 4f55235aa2f6801b3b1e2a08ef2971df7bcb0c93 Mon Sep 17 00:00:00 2001 From: David K <142583+neffs@users.noreply.github.com> Date: Sun, 29 Sep 2019 12:06:51 +0200 Subject: [PATCH 212/296] Return esphome cover position as Integer (#27039) cover position is specified as integer 0-100, we should not return float here. fixes #25738 --- homeassistant/components/esphome/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 7da2fcee3805ad..31b895b4eb2f55 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -91,11 +91,11 @@ def is_closing(self) -> bool: return self._state.current_operation == CoverOperation.IS_CLOSING @esphome_state_property - def current_cover_position(self) -> Optional[float]: + def current_cover_position(self) -> Optional[int]: """Return current position of cover. 0 is closed, 100 is open.""" if not self._static_info.supports_position: return None - return self._state.position * 100.0 + return round(self._state.position * 100.0) @esphome_state_property def current_cover_tilt_position(self) -> Optional[float]: From f259ff17d525cd945eb965440691b11a92ccb2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 29 Sep 2019 20:07:49 +0300 Subject: [PATCH 213/296] Type hint additions (#26831) * Type hint additions * Remove optional from sidebar_icon comment Co-Authored-By: Franck Nijhof * Remove optional from sidebar_title comment Co-Authored-By: Franck Nijhof * Fix issues after rebase and mypy 0.730 --- homeassistant/components/automation/state.py | 18 +++++++++---- .../components/device_automation/__init__.py | 6 ++++- .../device_automation/toggle_entity.py | 9 ++++--- homeassistant/components/frontend/__init__.py | 16 ++++++------ homeassistant/components/group/__init__.py | 14 +++++++--- homeassistant/components/group/cover.py | 26 +++++++++++++++---- homeassistant/components/group/light.py | 11 +++++--- homeassistant/components/group/notify.py | 3 +++ .../components/media_player/__init__.py | 5 ++-- .../persistent_notification/__init__.py | 20 +++++++++----- homeassistant/components/sun/__init__.py | 3 +++ .../components/websocket_api/__init__.py | 3 +++ .../components/websocket_api/auth.py | 8 +++++- .../components/websocket_api/commands.py | 3 +++ .../components/websocket_api/connection.py | 7 ++++- .../components/websocket_api/decorators.py | 2 ++ .../components/websocket_api/http.py | 19 ++++++++------ .../components/websocket_api/messages.py | 2 ++ .../components/websocket_api/sensor.py | 3 +++ homeassistant/components/zone/__init__.py | 13 +++++++--- homeassistant/components/zone/config_flow.py | 12 +++++++-- homeassistant/components/zone/zone.py | 13 ++++++++-- homeassistant/core.py | 3 ++- homeassistant/data_entry_flow.py | 12 ++++----- homeassistant/helpers/entity.py | 13 ++++++---- homeassistant/helpers/intent.py | 2 +- mypyrc | 6 +++++ 27 files changed, 184 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 184b9ea302b11c..154394075a02c6 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -1,16 +1,19 @@ """Offer state listening automation rules.""" +from datetime import timedelta import logging +from typing import Dict import voluptuous as vol from homeassistant import exceptions -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, CALLBACK_TYPE, callback from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import async_track_state_change, async_track_same_state -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -38,8 +41,13 @@ async def async_attach_trigger( - hass, config, action, automation_info, *, platform_type="state" -): + hass: HomeAssistant, + config, + action, + automation_info, + *, + platform_type: str = "state", +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) @@ -48,7 +56,7 @@ async def async_attach_trigger( template.attach(hass, time_delta) match_all = from_state == MATCH_ALL and to_state == MATCH_ALL unsub_track_same = {} - period = {} + period: Dict[str, timedelta] = {} @callback def state_automation_listener(entity, from_s, to_s): diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 62d338ece54a49..23e320fe153b60 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,6 +1,7 @@ """Helpers for device automations.""" import asyncio import logging +from typing import Any, List, MutableMapping import voluptuous as vol @@ -11,6 +12,9 @@ from .exceptions import InvalidDeviceAutomationConfig + +# mypy: allow-untyped-calls, allow-untyped-defs + DOMAIN = "device_automation" _LOGGER = logging.getLogger(__name__) @@ -96,7 +100,7 @@ async def _async_get_device_automations(hass, automation_type, device_id): ) domains = set() - automations = [] + automations: List[MutableMapping[str, Any]] = [] device = device_registry.async_get(device_id) for entry_id in device.config_entries: config_entry = hass.config_entries.async_get_entry(entry_id) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index b7cadd1349a3eb..ef1b605f4d68e0 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -1,5 +1,5 @@ """Device automation helpers for toggle entity.""" -from typing import List +from typing import Any, Dict, List import voluptuous as vol from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE @@ -19,6 +19,9 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import TRIGGER_BASE_SCHEMA + +# mypy: allow-untyped-calls, allow-untyped-defs + ENTITY_ACTIONS = [ { # Turn entity off @@ -88,7 +91,7 @@ async def async_call_action_from_config( variables: TemplateVarsType, context: Context, domain: str, -): +) -> None: """Change state based on configuration.""" config = ACTION_SCHEMA(config) action_type = config[CONF_TYPE] @@ -156,7 +159,7 @@ async def _async_get_automations( hass: HomeAssistant, device_id: str, automation_templates: List[dict], domain: str ) -> List[dict]: """List device automations.""" - automations = [] + automations: List[Dict[str, Any]] = [] entity_registry = await hass.helpers.entity_registry.async_get_registry() entries = [ diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8ef662ec878f90..e46423c8271c96 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -4,7 +4,7 @@ import mimetypes import os import pathlib -from typing import Optional, Set, Tuple +from typing import Any, Dict, Optional, Set, Tuple from aiohttp import web, web_urldispatcher, hdrs import voluptuous as vol @@ -122,19 +122,19 @@ class Panel: """Abstract class for panels.""" # Name of the webcomponent - component_name = None + component_name: Optional[str] = None - # Icon to show in the sidebar (optional) - sidebar_icon = None + # Icon to show in the sidebar + sidebar_icon: Optional[str] = None - # Title to show in the sidebar (optional) - sidebar_title = None + # Title to show in the sidebar + sidebar_title: Optional[str] = None # Url to show the panel in the frontend - frontend_url_path = None + frontend_url_path: Optional[str] = None # Config to pass to the webcomponent - config = None + config: Optional[Dict[str, Any]] = None # If the panel should only be visible to admins require_admin = False diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 75b45471982b49..204fcab0381aeb 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -1,6 +1,7 @@ """Provide the functionality to group entities.""" import asyncio import logging +from typing import Any, Iterable, List, Optional, cast import voluptuous as vol @@ -32,9 +33,12 @@ from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.async_ import run_coroutine_threadsafe +# mypy: allow-untyped-calls, allow-untyped-defs + DOMAIN = "group" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -143,12 +147,12 @@ def is_on(hass, entity_id): @bind_hass -def expand_entity_ids(hass, entity_ids): +def expand_entity_ids(hass: HomeAssistantType, entity_ids: Iterable[Any]) -> List[str]: """Return entity_ids with group entity ids replaced by their members. Async friendly. """ - found_ids = [] + found_ids: List[str] = [] for entity_id in entity_ids: if not isinstance(entity_id, str): continue @@ -182,7 +186,9 @@ def expand_entity_ids(hass, entity_ids): @bind_hass -def get_entity_ids(hass, entity_id, domain_filter=None): +def get_entity_ids( + hass: HomeAssistantType, entity_id: str, domain_filter: Optional[str] = None +) -> List[str]: """Get members of this group. Async friendly. @@ -194,7 +200,7 @@ def get_entity_ids(hass, entity_id, domain_filter=None): entity_ids = group.attributes[ATTR_ENTITY_ID] if not domain_filter: - return entity_ids + return cast(List[str], entity_ids) domain_filter = domain_filter.lower() + "." diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index faa4ddfc87d608..c5200082f2fb25 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -1,5 +1,6 @@ """This platform allows several cover to be grouped into one cover.""" import logging +from typing import Dict, Optional, Set import voluptuous as vol @@ -11,7 +12,7 @@ CONF_NAME, STATE_CLOSED, ) -from homeassistant.core import callback +from homeassistant.core import callback, State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change @@ -41,6 +42,9 @@ CoverDevice, ) + +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) KEY_OPEN_CLOSE = "open_close" @@ -76,13 +80,25 @@ def __init__(self, name, entities): self._assumed_state = True self._entities = entities - self._covers = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), KEY_POSITION: set()} - self._tilts = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), KEY_POSITION: set()} + self._covers: Dict[str, Set[str]] = { + KEY_OPEN_CLOSE: set(), + KEY_STOP: set(), + KEY_POSITION: set(), + } + self._tilts: Dict[str, Set[str]] = { + KEY_OPEN_CLOSE: set(), + KEY_STOP: set(), + KEY_POSITION: set(), + } @callback def update_supported_features( - self, entity_id, old_state, new_state, update_state=True - ): + self, + entity_id: str, + old_state: Optional[State], + new_state: Optional[State], + update_state: bool = True, + ) -> None: """Update dictionaries with supported features.""" if not new_state: for values in self._covers.values(): diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 0b1291d4045f42..e77c858fc0272a 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -3,7 +3,7 @@ from collections import Counter import itertools import logging -from typing import Any, Callable, Iterator, List, Optional, Tuple +from typing import Any, Callable, Iterator, List, Optional, Tuple, cast import voluptuous as vol @@ -43,6 +43,9 @@ SUPPORT_WHITE_VALUE, ) + +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Light Group" @@ -69,7 +72,9 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Initialize light.group platform.""" - async_add_entities([LightGroup(config.get(CONF_NAME), config[CONF_ENTITIES])]) + async_add_entities( + [LightGroup(cast(str, config.get(CONF_NAME)), config[CONF_ENTITIES])] + ) class LightGroup(light.Light): @@ -263,7 +268,7 @@ async def async_turn_off(self, **kwargs): async def async_update(self): """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] - states = list(filter(None, all_states)) + states: List[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] self._is_on = len(on_states) > 0 diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 3d3c644fea9673..2ffb7fea049438 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -17,6 +17,9 @@ BaseNotificationService, ) + +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) CONF_SERVICES = "services" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 791dacb70243c9..98da19fd98efe0 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -7,6 +7,7 @@ import hashlib import logging from random import SystemRandom +from typing import Optional from urllib.parse import urlparse from aiohttp import web @@ -347,7 +348,7 @@ async def async_unload_entry(hass, entry): class MediaPlayerDevice(Entity): """ABC for media player devices.""" - _access_token = None + _access_token: Optional[str] = None # Implement these for your media player @property @@ -356,7 +357,7 @@ def state(self): return None @property - def access_token(self): + def access_token(self) -> str: """Access token for this media player.""" if self._access_token is None: self._access_token = hashlib.sha256( diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 6c49784ededcb2..6b9c7c44ddf5ce 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -1,7 +1,7 @@ """Support for displaying persistent notifications.""" from collections import OrderedDict import logging -from typing import Awaitable +from typing import Any, Mapping, MutableMapping, Optional import voluptuous as vol @@ -14,6 +14,9 @@ from homeassistant.util import slugify import homeassistant.util.dt as dt_util + +# mypy: allow-untyped-calls, allow-untyped-defs + ATTR_CREATED_AT = "created_at" ATTR_MESSAGE = "message" ATTR_NOTIFICATION_ID = "notification_id" @@ -70,7 +73,10 @@ def dismiss(hass, notification_id): @callback @bind_hass def async_create( - hass: HomeAssistant, message: str, title: str = None, notification_id: str = None + hass: HomeAssistant, + message: str, + title: Optional[str] = None, + notification_id: Optional[str] = None, ) -> None: """Generate a notification.""" data = { @@ -95,9 +101,9 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None: hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_DISMISS, data)) -async def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the persistent notification component.""" - persistent_notifications = OrderedDict() + persistent_notifications: MutableMapping[str, MutableMapping] = OrderedDict() hass.data[DOMAIN] = {"notifications": persistent_notifications} @callback @@ -201,8 +207,10 @@ def mark_read_service(call): @callback def websocket_get_notifications( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: Mapping[str, Any], +) -> None: """Return a list of persistent_notifications.""" connection.send_message( websocket_api.result_message( diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 3e6048e15320b6..7d883e273e5053 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -17,6 +17,9 @@ ) from homeassistant.util import dt as dt_util + +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "sun" diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 57ab34a6a5a409..1ec758ebd4d1a5 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -4,6 +4,9 @@ from . import commands, connection, const, decorators, http, messages + +# mypy: allow-untyped-calls, allow-untyped-defs + DOMAIN = const.DOMAIN DEPENDENCIES = ("http",) diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 2d748f5cc6aca3..716b20f4ca4068 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -2,6 +2,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant.auth.models import RefreshToken, User from homeassistant.auth.providers import legacy_api_password from homeassistant.components.http.ban import process_wrong_login, process_success_login from homeassistant.const import __version__ @@ -9,6 +10,9 @@ from .connection import ActiveConnection from .error import Disconnect + +# mypy: allow-untyped-calls, allow-untyped-defs + TYPE_AUTH = "auth" TYPE_AUTH_INVALID = "auth_invalid" TYPE_AUTH_OK = "auth_ok" @@ -87,7 +91,9 @@ async def async_handle(self, msg): await process_wrong_login(self._request) raise Disconnect - async def _async_finish_auth(self, user, refresh_token) -> ActiveConnection: + async def _async_finish_auth( + self, user: User, refresh_token: RefreshToken + ) -> ActiveConnection: """Create an active connection.""" self._logger.debug("Auth OK") await process_success_login(self._request) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index deb3600574fb4b..9d46238b24104f 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -12,6 +12,9 @@ from . import const, decorators, messages +# mypy: allow-untyped-calls, allow-untyped-defs + + @callback def async_register_commands(hass, async_reg): """Register commands.""" diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 3886d6c21d081e..41232b097d14e1 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -1,5 +1,7 @@ """Connection session.""" import asyncio +from typing import Any, Callable, Dict, Hashable + import voluptuous as vol from homeassistant.core import callback, Context @@ -8,6 +10,9 @@ from . import const, messages +# mypy: allow-untyped-calls, allow-untyped-defs + + class ActiveConnection: """Handle an active websocket client connection.""" @@ -22,7 +27,7 @@ def __init__(self, logger, hass, send_message, user, refresh_token): else: self.refresh_token_id = None - self.subscriptions = {} + self.subscriptions: Dict[Hashable, Callable[[], Any]] = {} self.last_id = 0 def context(self, msg): diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index ba65d5e19a843e..025131643e894a 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -8,6 +8,8 @@ from . import messages +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 9a1f375fdfda20..17a6709496a4e6 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -25,6 +25,9 @@ from .messages import error_message +# mypy: allow-untyped-calls, allow-untyped-defs + + class WebsocketAPIView(HomeAssistantView): """View to serve a websockets endpoint.""" @@ -45,7 +48,7 @@ def __init__(self, hass, request): self.hass = hass self.request = request self.wsock = None - self._to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG) + self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG) self._handle_task = None self._writer_task = None self._logger = logging.getLogger("{}.connection.{}".format(__name__, id(self))) @@ -106,7 +109,7 @@ async def async_handle(self): # Py3.7+ if hasattr(asyncio, "current_task"): # pylint: disable=no-member - self._handle_task = asyncio.current_task() + self._handle_task = asyncio.current_task() # type: ignore else: self._handle_task = asyncio.Task.current_task() @@ -144,13 +147,13 @@ def handle_hass_stop(event): raise Disconnect try: - msg = msg.json() + msg_data = msg.json() except ValueError: disconnect_warn = "Received invalid JSON." raise Disconnect - self._logger.debug("Received %s", msg) - connection = await auth.async_handle(msg) + self._logger.debug("Received %s", msg_data) + connection = await auth.async_handle(msg_data) self.hass.data[DATA_CONNECTIONS] = ( self.hass.data.get(DATA_CONNECTIONS, 0) + 1 ) @@ -170,13 +173,13 @@ def handle_hass_stop(event): break try: - msg = msg.json() + msg_data = msg.json() except ValueError: disconnect_warn = "Received invalid JSON." break - self._logger.debug("Received %s", msg) - connection.async_handle(msg) + self._logger.debug("Received %s", msg_data) + connection.async_handle(msg_data) except asyncio.CancelledError: self._logger.info("Connection closed by client") diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 65291bc55e7ee9..c8c760a6549560 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -7,6 +7,8 @@ from . import const +# mypy: allow-untyped-defs + # Minimal requirements of a message MINIMAL_MESSAGE_SCHEMA = vol.Schema( {vol.Required("id"): cv.positive_int, vol.Required("type"): cv.string}, diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 1ae76b562525c8..20a6a90860be42 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -10,6 +10,9 @@ ) +# mypy: allow-untyped-calls, allow-untyped-defs + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the API streams platform.""" entity = APICount() diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 2ee03c08189aea..6ae62be3eb9b2d 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -1,9 +1,10 @@ """Support for the definition of zones.""" import logging +from typing import Set, cast import voluptuous as vol -from homeassistant.core import callback +from homeassistant.core import callback, State from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.const import ( @@ -25,6 +26,9 @@ from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE, ATTR_PASSIVE, ATTR_RADIUS from .zone import Zone + +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Unnamed zone" @@ -78,10 +82,11 @@ def async_active_zone(hass, latitude, longitude, radius=0): ) within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS] - closer_zone = closest is None or zone_dist < min_dist + closer_zone = closest is None or zone_dist < min_dist # type: ignore smaller_zone = ( zone_dist == min_dist - and zone.attributes[ATTR_RADIUS] < closest.attributes[ATTR_RADIUS] + and zone.attributes[ATTR_RADIUS] + < cast(State, closest).attributes[ATTR_RADIUS] ) if within_zone and (closer_zone or smaller_zone): @@ -94,7 +99,7 @@ def async_active_zone(hass, latitude, longitude, radius=0): async def async_setup(hass, config): """Set up configured zones as well as home assistant zone if necessary.""" hass.data[DOMAIN] = {} - entities = set() + entities: Set[str] = set() zone_entries = configured_zones(hass) for _, entry in config_per_platform(config, DOMAIN): if slugify(entry[CONF_NAME]) not in zone_entries: diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index 05ba28e4ca7360..d23fb5a47577ca 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -1,5 +1,7 @@ """Config flow to configure zone component.""" +from typing import Set + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -12,17 +14,23 @@ CONF_RADIUS, ) from homeassistant.core import callback +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE +# mypy: allow-untyped-defs + + @callback -def configured_zones(hass): +def configured_zones(hass: HomeAssistantType) -> Set[str]: """Return a set of the configured zones.""" return set( (slugify(entry.data[CONF_NAME])) - for entry in hass.config_entries.async_entries(DOMAIN) + for entry in ( + hass.config_entries.async_entries(DOMAIN) if hass.config_entries else [] + ) ) diff --git a/homeassistant/components/zone/zone.py b/homeassistant/components/zone/zone.py index ccd8e55a4ce4d5..f084492bd34fe8 100644 --- a/homeassistant/components/zone/zone.py +++ b/homeassistant/components/zone/zone.py @@ -1,5 +1,9 @@ """Zone entity and functionality.""" + +from typing import cast + from homeassistant.const import ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import State from homeassistant.helpers.entity import Entity from homeassistant.util.location import distance @@ -8,7 +12,10 @@ STATE = "zoning" -def in_zone(zone, latitude, longitude, radius=0) -> bool: +# mypy: allow-untyped-defs + + +def in_zone(zone: State, latitude: float, longitude: float, radius: float = 0) -> bool: """Test if given latitude, longitude is in given zone. Async friendly. @@ -20,7 +27,9 @@ def in_zone(zone, latitude, longitude, radius=0) -> bool: zone.attributes[ATTR_LONGITUDE], ) - return zone_dist - radius < zone.attributes[ATTR_RADIUS] + if zone_dist is None or zone.attributes[ATTR_RADIUS] is None: + return False + return zone_dist - radius < cast(float, zone.attributes[ATTR_RADIUS]) class Zone(Entity): diff --git a/homeassistant/core.py b/homeassistant/core.py index e011db33c343a8..f4be3b66323d1b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -28,6 +28,7 @@ Set, TYPE_CHECKING, Awaitable, + Mapping, ) from async_timeout import timeout @@ -704,7 +705,7 @@ def __init__( self, entity_id: str, state: str, - attributes: Optional[Dict] = None, + attributes: Optional[Mapping] = None, last_changed: Optional[datetime.datetime] = None, last_updated: Optional[datetime.datetime] = None, context: Optional[Context] = None, diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 3b128646219db2..0bc27498f767e8 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -170,7 +170,7 @@ class FlowHandler: # Set by flow manager flow_id: Optional[str] = None hass: Optional[HomeAssistant] = None - handler = None + handler: Optional[Hashable] = None cur_step: Optional[Dict[str, str]] = None context: Dict @@ -188,7 +188,7 @@ def async_show_form( data_schema: vol.Schema = None, errors: Optional[Dict] = None, description_placeholders: Optional[Dict] = None, - ) -> Dict: + ) -> Dict[str, Any]: """Return the definition of a form to gather user input.""" return { "type": RESULT_TYPE_FORM, @@ -208,7 +208,7 @@ def async_create_entry( data: Dict, description: Optional[str] = None, description_placeholders: Optional[Dict] = None, - ) -> Dict: + ) -> Dict[str, Any]: """Finish config flow and create a config entry.""" return { "version": self.VERSION, @@ -224,7 +224,7 @@ def async_create_entry( @callback def async_abort( self, *, reason: str, description_placeholders: Optional[Dict] = None - ) -> Dict: + ) -> Dict[str, Any]: """Abort the config flow.""" return { "type": RESULT_TYPE_ABORT, @@ -237,7 +237,7 @@ def async_abort( @callback def async_external_step( self, *, step_id: str, url: str, description_placeholders: Optional[Dict] = None - ) -> Dict: + ) -> Dict[str, Any]: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP, @@ -249,7 +249,7 @@ def async_external_step( } @callback - def async_external_step_done(self, *, next_step_id: str) -> Dict: + def async_external_step_done(self, *, next_step_id: str) -> Dict[str, Any]: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP_DONE, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index fad02dee075839..836ad954ae0c6b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,5 +1,7 @@ """An abstract class for entities.""" -from datetime import timedelta + +import asyncio +from datetime import datetime, timedelta import logging import functools as ft from timeit import default_timer as timer @@ -22,6 +24,7 @@ ATTR_SUPPORTED_FEATURES, ATTR_DEVICE_CLASS, ) +from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import ( EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry, @@ -94,7 +97,7 @@ class Entity: hass: Optional[HomeAssistant] = None # Owning platform instance. Will be set by EntityPlatform - platform = None + platform: Optional[EntityPlatform] = None # If we reported if this entity was slow _slow_reported = False @@ -106,7 +109,7 @@ class Entity: _update_staged = False # Process updates in parallel - parallel_updates = None + parallel_updates: Optional[asyncio.Semaphore] = None # Entry in the entity registry registry_entry: Optional[RegistryEntry] = None @@ -115,8 +118,8 @@ class Entity: _on_remove: Optional[List[CALLBACK_TYPE]] = None # Context - _context = None - _context_set = None + _context: Optional[Context] = None + _context_set: Optional[datetime] = None @property def should_poll(self) -> bool: diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 1fa0ec76a67c1b..dc48d825348f04 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -124,7 +124,7 @@ class IntentHandler: intent_type: Optional[str] = None slot_schema: Optional[vol.Schema] = None - _slot_schema = None + _slot_schema: Optional[vol.Schema] = None platforms: Optional[Iterable[str]] = [] @callback diff --git a/mypyrc b/mypyrc index f3866f40e5743c..08413ecd23c696 100644 --- a/mypyrc +++ b/mypyrc @@ -6,8 +6,10 @@ homeassistant/components/binary_sensor/ homeassistant/components/calendar/ homeassistant/components/camera/ homeassistant/components/cover/ +homeassistant/components/device_automation/ homeassistant/components/frontend/ homeassistant/components/geo_location/ +homeassistant/components/group/ homeassistant/components/history/ homeassistant/components/http/ homeassistant/components/image_processing/ @@ -17,16 +19,20 @@ homeassistant/components/lock/ homeassistant/components/mailbox/ homeassistant/components/media_player/ homeassistant/components/notify/ +homeassistant/components/persistent_notification/ homeassistant/components/proximity/ homeassistant/components/remote/ homeassistant/components/scene/ homeassistant/components/sensor/ +homeassistant/components/sun/ homeassistant/components/switch/ homeassistant/components/systemmonitor/ homeassistant/components/tts/ homeassistant/components/vacuum/ homeassistant/components/water_heater/ homeassistant/components/weather/ +homeassistant/components/websocket_api/ +homeassistant/components/zone/ homeassistant/helpers/ homeassistant/scripts/ homeassistant/util/ From 52bbb6242cfdebf29adbcf608fbd52bf7858d9b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 30 Sep 2019 00:00:39 +0300 Subject: [PATCH 214/296] Upgrade pytest to 5.2.0 (#27058) https://docs.pytest.org/en/latest/changelog.html#pytest-5-2-0-2019-09-28 --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 7e5be09a28ba9c..9da375b33c8a91 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -18,5 +18,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.1.3 +pytest==5.2.0 requests_mock==1.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9599d24b20a1b7..2701513a6de7b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.1.3 +pytest==5.2.0 requests_mock==1.7.0 From cdb469f711310d75f805261df99aa37aeb723f2e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 30 Sep 2019 00:32:17 +0000 Subject: [PATCH 215/296] [ci skip] Translation update --- .../ambient_station/.translations/es.json | 2 +- .../arcam_fmj/.translations/pt-BR.json | 5 + .../binary_sensor/.translations/ca.json | 27 +++ .../binary_sensor/.translations/es.json | 92 ++++++++++ .../binary_sensor/.translations/pl.json | 168 +++++++++--------- .../cert_expiry/.translations/pt-BR.json | 21 +++ .../components/deconz/.translations/pl.json | 34 ++-- .../dialogflow/.translations/es.json | 2 +- .../components/ecobee/.translations/ca.json | 25 +++ .../components/ecobee/.translations/es.json | 25 +++ .../components/ecobee/.translations/sl.json | 25 +++ .../components/izone/.translations/es.json | 15 ++ .../components/light/.translations/pl.json | 14 +- .../components/plex/.translations/ca.json | 11 ++ .../components/plex/.translations/es.json | 56 ++++++ .../components/plex/.translations/sl.json | 11 ++ .../plex/.translations/zh-Hant.json | 11 ++ .../components/switch/.translations/pl.json | 18 +- .../traccar/.translations/pt-BR.json | 5 + .../transmission/.translations/ca.json | 40 +++++ .../transmission/.translations/es.json | 40 +++++ .../transmission/.translations/pt-BR.json | 37 ++++ .../transmission/.translations/sl.json | 40 +++++ .../transmission/.translations/zh-Hant.json | 40 +++++ .../components/unifi/.translations/pt-BR.json | 10 ++ .../components/withings/.translations/es.json | 3 + .../components/zha/.translations/ca.json | 47 +++++ .../components/zha/.translations/es.json | 47 +++++ .../components/zha/.translations/pl.json | 60 +++---- .../components/zha/.translations/pt-BR.json | 5 + .../components/zha/.translations/sl.json | 47 +++++ .../components/zha/.translations/zh-Hant.json | 4 + 32 files changed, 838 insertions(+), 149 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/pt-BR.json create mode 100644 homeassistant/components/binary_sensor/.translations/es.json create mode 100644 homeassistant/components/cert_expiry/.translations/pt-BR.json create mode 100644 homeassistant/components/ecobee/.translations/ca.json create mode 100644 homeassistant/components/ecobee/.translations/es.json create mode 100644 homeassistant/components/ecobee/.translations/sl.json create mode 100644 homeassistant/components/izone/.translations/es.json create mode 100644 homeassistant/components/plex/.translations/es.json create mode 100644 homeassistant/components/traccar/.translations/pt-BR.json create mode 100644 homeassistant/components/transmission/.translations/ca.json create mode 100644 homeassistant/components/transmission/.translations/es.json create mode 100644 homeassistant/components/transmission/.translations/pt-BR.json create mode 100644 homeassistant/components/transmission/.translations/sl.json create mode 100644 homeassistant/components/transmission/.translations/zh-Hant.json diff --git a/homeassistant/components/ambient_station/.translations/es.json b/homeassistant/components/ambient_station/.translations/es.json index d4b0075aa6576e..d4222f1d2ebea9 100644 --- a/homeassistant/components/ambient_station/.translations/es.json +++ b/homeassistant/components/ambient_station/.translations/es.json @@ -14,6 +14,6 @@ "title": "Completa tu informaci\u00f3n" } }, - "title": "Ambient PWS" + "title": "Ambiente PWS" } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/pt-BR.json b/homeassistant/components/arcam_fmj/.translations/pt-BR.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ca.json b/homeassistant/components/binary_sensor/.translations/ca.json index 434c236418b37d..de7d837b12cc0d 100644 --- a/homeassistant/components/binary_sensor/.translations/ca.json +++ b/homeassistant/components/binary_sensor/.translations/ca.json @@ -2,29 +2,43 @@ "device_automation": { "condition_type": { "is_bat_low": "Bateria de {entity_name} baixa", + "is_cold": "{entity_name} est\u00e0 fred", "is_connected": "{entity_name} est\u00e0 connectat", "is_gas": "{entity_name} est\u00e0 detectant gas", + "is_hot": "{entity_name} est\u00e0 calent", "is_light": "{entity_name} est\u00e0 detectant llum", "is_locked": "{entity_name} est\u00e0 bloquejat", + "is_moist": "{entity_name} est\u00e0 humit", "is_motion": "{entity_name} est\u00e0 detectant moviment", + "is_moving": "{entity_name} s'est\u00e0 movent", "is_no_gas": "{entity_name} no detecta gas", "is_no_light": "{entity_name} no detecta llum", "is_no_motion": "{entity_name} no detecta moviment", + "is_no_problem": "{entity_name} no est\u00e0 detectant cap problema", "is_no_smoke": "{entity_name} no detecta fum", "is_no_sound": "{entity_name} no detecta so", "is_no_vibration": "{entity_name} no detecta vibraci\u00f3", "is_not_bat_low": "Bateria de {entity_name} normal", + "is_not_cold": "{entity_name} no est\u00e0 fred", "is_not_connected": "{entity_name} est\u00e0 desconnectat", + "is_not_hot": "{entity_name} no est\u00e0 calent", "is_not_locked": "{entity_name} est\u00e0 desbloquejat", + "is_not_moist": "{entity_name} est\u00e0 sec", + "is_not_moving": "{entity_name} no s'est\u00e0 movent", "is_not_occupied": "{entity_name} no est\u00e0 ocupat", + "is_not_open": "{entity_name} est\u00e0 tancat", + "is_not_plugged_in": "{entity_name} est\u00e0 desendollat", "is_not_powered": "{entity_name} no est\u00e0 alimentat", "is_not_present": "{entity_name} no est\u00e0 present", + "is_not_unsafe": "{entity_name} \u00e9s segur", "is_occupied": "{entity_name} est\u00e0 ocupat", "is_off": "{entity_name} est\u00e0 apagat", "is_on": "{entity_name} est\u00e0 enc\u00e8s", "is_open": "{entity_name} est\u00e0 obert", + "is_plugged_in": "{entity_name} est\u00e0 endollat", "is_powered": "{entity_name} est\u00e0 alimentat", "is_present": "{entity_name} est\u00e0 present", + "is_problem": "{entity_name} est\u00e0 detectant un problema", "is_smoke": "{entity_name} est\u00e0 detectant fum", "is_sound": "{entity_name} est\u00e0 detectant so", "is_unsafe": "{entity_name} \u00e9s insegur", @@ -33,10 +47,13 @@ "trigger_type": { "bat_low": "Bateria de {entity_name} baixa", "closed": "{entity_name} est\u00e0 tancat", + "cold": "{entity_name} es torna fred", "connected": "{entity_name} est\u00e0 connectat", "gas": "{entity_name} ha comen\u00e7at a detectar gas", + "hot": "{entity_name} es torna calent", "light": "{entity_name} ha comen\u00e7at a detectar llum", "locked": "{entity_name} est\u00e0 bloquejat", + "moist\u00a7": "{entity_name} es torna humit", "motion": "{entity_name} ha comen\u00e7at a detectar moviment", "moving": "{entity_name} ha comen\u00e7at a moure's", "no_gas": "{entity_name} ha deixat de detectar gas", @@ -47,11 +64,20 @@ "no_sound": "{entity_name} ha deixat de detectar so", "no_vibration": "{entity_name} ha deixat de detectar vibraci\u00f3", "not_bat_low": "Bateria de {entity_name} normal", + "not_cold": "{entity_name} es torna no-fred", "not_connected": "{entity_name} est\u00e0 desconnectat", + "not_hot": "{entity_name} es torna no-calent", "not_locked": "{entity_name} est\u00e0 desbloquejat", + "not_moist": "{entity_name} es torna sec", "not_moving": "{entity_name} ha parat de moure's", + "not_occupied": "{entity_name} es desocupa", + "not_plugged_in": "{entity_name} desendollat", "not_powered": "{entity_name} no est\u00e0 alimentat", "not_present": "{entity_name} no est\u00e0 present", + "not_unsafe": "{entity_name} es torna segur", + "occupied": "{entity_name} s'ocupa", + "opened": "{entity_name} s'ha obert", + "plugged_in": "{entity_name} s'ha endollat", "powered": "{entity_name} alimentat", "present": "{entity_name} present", "problem": "{entity_name} ha comen\u00e7at a detectar un problema", @@ -59,6 +85,7 @@ "sound": "{entity_name} ha comen\u00e7at a detectar so", "turned_off": "{entity_name} apagat", "turned_on": "{entity_name} enc\u00e8s", + "unsafe": "{entity_name} es torna insegur", "vibration": "{entity_name} ha comen\u00e7at a detectar vibraci\u00f3" } } diff --git a/homeassistant/components/binary_sensor/.translations/es.json b/homeassistant/components/binary_sensor/.translations/es.json new file mode 100644 index 00000000000000..8e2d326d9d3fe9 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/es.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} la bater\u00eda est\u00e1 baja", + "is_cold": "{entity_name} est\u00e1 fr\u00edo", + "is_connected": "{entity_name} est\u00e1 conectado", + "is_gas": "{entity_name} est\u00e1 detectando gas", + "is_hot": "{entity_name} est\u00e1 caliente", + "is_light": "{entity_name} est\u00e1 detectando luz", + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_moist": "{entity_name} est\u00e1 h\u00famedo", + "is_motion": "{entity_name} est\u00e1 detectando movimiento", + "is_moving": "{entity_name} se est\u00e1 moviendo", + "is_no_gas": "{entity_name} no detecta gas", + "is_no_light": "{entity_name} no detecta la luz", + "is_no_motion": "{entity_name} no detecta movimiento", + "is_no_problem": "{entity_name} no detecta el problema", + "is_no_smoke": "{entity_name} no detecta humo", + "is_no_sound": "{entity_name} no detecta sonido", + "is_no_vibration": "{entity_name} no detecta vibraci\u00f3n", + "is_not_bat_low": "La bater\u00eda de {entity_name} es normal", + "is_not_cold": "{entity_name} no est\u00e1 fr\u00edo", + "is_not_connected": "{entity_name} est\u00e1 desconectado", + "is_not_hot": "{entity_name} no est\u00e1 caliente", + "is_not_locked": "{entity_name} est\u00e1 desbloqueado", + "is_not_moist": "{entity_name} est\u00e1 seco", + "is_not_moving": "{entity_name} no se mueve", + "is_not_occupied": "{entity_name} no est\u00e1 ocupado", + "is_not_open": "{entity_name} est\u00e1 cerrado", + "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_not_powered": "{entity_name} no tiene alimentaci\u00f3n", + "is_not_present": "{entity_name} no est\u00e1 presente", + "is_not_unsafe": "{entity_name} es seguro", + "is_occupied": "{entity_name} est\u00e1 ocupado", + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 activado", + "is_open": "{entity_name} est\u00e1 abierto", + "is_plugged_in": "{entity_name} est\u00e1 conectado", + "is_powered": "{entity_name} est\u00e1 activado", + "is_present": "{entity_name} est\u00e1 presente", + "is_problem": "{entity_name} est\u00e1 detectando un problema", + "is_smoke": "{entity_name} est\u00e1 detectando humo", + "is_sound": "{entity_name} est\u00e1 detectando sonido", + "is_unsafe": "{entity_name} no es seguro", + "is_vibration": "{entity_name} est\u00e1 detectando vibraciones" + }, + "trigger_type": { + "bat_low": "{entity_name} bater\u00eda baja", + "closed": "{entity_name} cerrado", + "cold": "{entity_name} se enfri\u00f3", + "connected": "{entity_name} conectado", + "gas": "{entity_name} empez\u00f3 a detectar gas", + "hot": "{entity_name} se est\u00e1 calentando", + "light": "{entity_name} empez\u00f3 a detectar la luz", + "locked": "{entity_name} bloqueado", + "moist\u00a7": "{entity_name} se humedeci\u00f3", + "motion": "{entity_name} comenz\u00f3 a detectar movimiento", + "moving": "{entity_name} empez\u00f3 a moverse", + "no_gas": "{entity_name} dej\u00f3 de detectar gas", + "no_light": "{entity_name} dej\u00f3 de detectar la luz", + "no_motion": "{entity_name} dej\u00f3 de detectar movimiento", + "no_problem": "{entity_name} dej\u00f3 de detectar el problema", + "no_smoke": "{entity_name} dej\u00f3 de detectar humo", + "no_sound": "{entity_name} dej\u00f3 de detectar sonido", + "no_vibration": "{entity_name} dej\u00f3 de detectar vibraci\u00f3n", + "not_bat_low": "{entity_name} bater\u00eda normal", + "not_cold": "{entity_name} no se enfri\u00f3", + "not_connected": "{entity_name} desconectado", + "not_hot": "{entity_name} no se calent\u00f3", + "not_locked": "{entity_name} desbloqueado", + "not_moist": "{entity_name} se sec\u00f3", + "not_moving": "{entity_name} dej\u00f3 de moverse", + "not_occupied": "{entity_name} no est\u00e1 ocupado", + "not_plugged_in": "{entity_name} desconectado", + "not_powered": "{entity_name} no est\u00e1 activado", + "not_present": "{entity_name} no est\u00e1 presente", + "not_unsafe": "{entity_name} se volvi\u00f3 seguro", + "occupied": "{entity_name} se convirti\u00f3 en ocupado", + "opened": "{entity_name} abierto", + "plugged_in": "{nombre_de_la_entidad} conectado", + "powered": "{entity_name} alimentado", + "present": "{entity_name} presente", + "problem": "{entity_name} empez\u00f3 a detectar problemas", + "smoke": "{entity_name} empez\u00f3 a detectar humo", + "sound": "{entity_name} empez\u00f3 a detectar sonido", + "turned_off": "{entity_name} desactivado", + "turned_on": "{entity_name} activado", + "unsafe": "{entity_name} se volvi\u00f3 inseguro", + "vibration": "{entity_name} empez\u00f3 a detectar vibraciones" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json index 059800a116fe9d..a7f0bd516a08f5 100644 --- a/homeassistant/components/binary_sensor/.translations/pl.json +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -1,92 +1,92 @@ { "device_automation": { "condition_type": { - "is_bat_low": "Bateria {entity_name} jest roz\u0142adowana", - "is_cold": "{entity_name} wykrywa zimno", - "is_connected": "{entity_name} jest po\u0142\u0105czony", - "is_gas": "{entity_name} wykrywa gaz", - "is_hot": "{entity_name} wykrywa gor\u0105co", - "is_light": "{entity_name} wykrywa \u015bwiat\u0142o", - "is_locked": "{entity_name} jest zamkni\u0119ty", - "is_moist": "{entity_name} wykrywa wilgo\u0107", - "is_motion": "{entity_name} wykrywa ruch", - "is_moving": "{entity_name} porusza si\u0119", - "is_no_gas": "{entity_name} nie wykrywa gazu", - "is_no_light": "{entity_name} nie wykrywa \u015bwiat\u0142a", - "is_no_motion": "{entity_name} nie wykrywa ruchu", - "is_no_problem": "{entity_name} nie wykrywa problemu", - "is_no_smoke": "{entity_name} nie wykrywa dymu", - "is_no_sound": "{entity_name} nie wykrywa d\u017awi\u0119ku", - "is_no_vibration": "{entity_name} nie wykrywa wibracji", - "is_not_bat_low": "Bateria {entity_name} nie jest roz\u0142adowana", - "is_not_cold": "{entity_name} nie wykrywa zimna", - "is_not_connected": "{entity_name} jest roz\u0142\u0105czony", - "is_not_hot": "{entity_name} nie wykrywa gor\u0105ca", - "is_not_locked": "{entity_name} jest otwarty", - "is_not_moist": "{entity_name} nie wykrywa wilgoci", - "is_not_moving": "{entity_name} nie porusza si\u0119", - "is_not_occupied": "{entity_name} nie jest zaj\u0119ty", - "is_not_open": "{entity_name} jest zamkni\u0119ty", - "is_not_plugged_in": "{entity_name} jest od\u0142\u0105czony", - "is_not_powered": "{entity_name} nie jest zasilany", - "is_not_present": "{entity_name} nie wykrywa obecno\u015bci", - "is_not_unsafe": "{entity_name} raportuje bezpiecze\u0144stwo", - "is_occupied": "{entity_name} jest zaj\u0119ty", - "is_off": "{entity_name} jest wy\u0142\u0105czone", - "is_on": "{entity_name} jest w\u0142\u0105czone", - "is_open": "{entity_name} jest otwarty", - "is_plugged_in": "{entity_name} jest pod\u0142\u0105czony", - "is_powered": "{entity_name} jest zasilany", - "is_present": "{entity_name} wykrywa obecno\u015b\u0107", - "is_problem": "{entity_name} wykrywa problem", - "is_smoke": "{entity_name} wykrywa dym", - "is_sound": "{entity_name} wykrywa d\u017awi\u0119k", - "is_unsafe": "{entity_name} raportuje niebezpiecze\u0144stwo", - "is_vibration": "{entity_name} wykrywa wibracje" + "is_bat_low": "bateria {entity_name} jest roz\u0142adowana", + "is_cold": "sensor {entity_name} wykrywa zimno", + "is_connected": "sensor {entity_name} raportuje po\u0142\u0105czenie", + "is_gas": "sensor {entity_name} wykrywa gaz", + "is_hot": "sensor {entity_name} wykrywa gor\u0105co", + "is_light": "sensor {entity_name} wykrywa \u015bwiat\u0142o", + "is_locked": "sensor {entity_name} wykrywa zamkni\u0119cie", + "is_moist": "sensor {entity_name} wykrywa wilgo\u0107", + "is_motion": "sensor {entity_name} wykrywa ruch", + "is_moving": "sensor {entity_name} porusza si\u0119", + "is_no_gas": "sensor {entity_name} nie wykrywa gazu", + "is_no_light": "sensor {entity_name} nie wykrywa \u015bwiat\u0142a", + "is_no_motion": "sensor {entity_name} nie wykrywa ruchu", + "is_no_problem": "sensor {entity_name} nie wykrywa problemu", + "is_no_smoke": "sensor {entity_name} nie wykrywa dymu", + "is_no_sound": "sensor {entity_name} nie wykrywa d\u017awi\u0119ku", + "is_no_vibration": "sensor {entity_name} nie wykrywa wibracji", + "is_not_bat_low": "bateria {entity_name} nie jest roz\u0142adowana", + "is_not_cold": "sensor {entity_name} nie wykrywa zimna", + "is_not_connected": "sensor {entity_name} nie wykrywa roz\u0142\u0105czenia", + "is_not_hot": "sensor {entity_name} nie wykrywa gor\u0105ca", + "is_not_locked": "sensor {entity_name} nie wykrywa otwarcia", + "is_not_moist": "sensor {entity_name} nie wykrywa wilgoci", + "is_not_moving": "sensor {entity_name} nie porusza si\u0119", + "is_not_occupied": "sensor {entity_name} nie jest zaj\u0119ty", + "is_not_open": "sensor {entity_name} jest zamkni\u0119ty", + "is_not_plugged_in": "sensor {entity_name} wykrywa od\u0142\u0105czenie", + "is_not_powered": "sensor {entity_name} nie wykrywa zasilania", + "is_not_present": "sensor {entity_name} nie wykrywa obecno\u015bci", + "is_not_unsafe": "sensor {entity_name} nie wykrywa niebezpiecze\u0144stwa", + "is_occupied": "sensor {entity_name} jest zaj\u0119ty", + "is_off": "sensor {entity_name} jest wy\u0142\u0105czony", + "is_on": "sensor {entity_name} jest w\u0142\u0105czony", + "is_open": "sensor {entity_name} jest otwarty", + "is_plugged_in": "sensor {entity_name} wykrywa pod\u0142\u0105czenie", + "is_powered": "sensor {entity_name} wykrywa zasilanie", + "is_present": "sensor {entity_name} wykrywa obecno\u015b\u0107", + "is_problem": "sensor {entity_name} wykrywa problem", + "is_smoke": "sensor {entity_name} wykrywa dym", + "is_sound": "sensor {entity_name} wykrywa d\u017awi\u0119k", + "is_unsafe": "sensor {entity_name} wykrywa niebezpiecze\u0144stwo", + "is_vibration": "sensor {entity_name} wykrywa wibracje" }, "trigger_type": { - "bat_low": "Bateria {entity_name} staje si\u0119 roz\u0142adowanie", - "closed": "Zamkni\u0119cie {entity_name}", - "cold": "Wykrycie zimna przez {entity_name}", - "connected": "Pod\u0142\u0105czenie {entity_name}", - "gas": "Wykrycie gazu przez {entity_name}", - "hot": "Wykrycie gor\u0105ca przez {entity_name}", - "light": "Wykrycie \u015bwiat\u0142a przez {entity_name}", - "locked": "Zamkni\u0119cie {entity_name}", - "moist\u00a7": "Wykrycie wilgoci przez {entity_name}", - "motion": "Wykrycie ruchu przez {entity_name}", - "moving": "{entity_name} zacz\u0105\u0142 si\u0119 porusza\u0107", - "no_gas": "Wykrycie braku gazu przez {entity_name}", - "no_light": "Wykrycie braku \u015bwiat\u0142a przez {entity_name}", - "no_motion": "Wykrycie braku ruchu przez {entity_name}", - "no_problem": "Wykrycie braku problemu przez {entity_name}", - "no_smoke": "Wykrycie braku dymu przez {entity_name}", - "no_sound": "Wykrycie braku d\u017awi\u0119ku przez {entity_name}", - "no_vibration": "Wykrycie braku wibracji przez {entity_name}", - "not_bat_low": "Bateria {entity_name} staje si\u0119 na\u0142adowana", - "not_cold": "Wykrycie braku zimna przez {entity_name}", - "not_connected": "Roz\u0142\u0105czenie {entity_name}", - "not_hot": "Wykrycie braku gor\u0105ca przez {entity_name}", - "not_locked": "Otwarcie {entity_name}", - "not_moist": "Wykrycie braku wilgoci przez {entity_name}", - "not_moving": "{entity_name} przesta\u0142 si\u0119 porusza\u0107", - "not_occupied": "{entity_name} sta\u0142 si\u0119 niezaj\u0119ty", - "not_plugged_in": "Od\u0142\u0105czenie {entity_name}", - "not_powered": "Brak zasilania dla {entity_name}", - "not_present": "Wykrycie braku obecno\u015bci przez {entity_name}", - "not_unsafe": "Raportowanie bezpiecze\u0144stwa przez {entity_name}", - "occupied": "{entity_name} sta\u0142 si\u0119 zaj\u0119ty", - "opened": "Otwarcie {entity_name}", - "plugged_in": "Pod\u0142\u0105czenie {entity_name}", - "powered": "Zasilenie {entity_name}", - "present": "Wykrycie obecno\u015bci przez {entity_name}", - "problem": "Wykrycie problemu przez {entity_name}", - "smoke": "Wykrycie dymu przez {entity_name}", - "sound": "Wykrycie d\u017awi\u0119ku przez {entity_name}", - "turned_off": "Wy\u0142\u0105czenie {entity_name}", - "turned_on": "W\u0142\u0105czenie {entity_name}", - "unsafe": "Raportowanie niebezpiecze\u0144stwa przez {entity_name}", - "vibration": "Wykrycie wibracji przez {entity_name}" + "bat_low": "bateria {entity_name} stanie si\u0119 roz\u0142adowana", + "closed": "zamkni\u0119cie {entity_name}", + "cold": "sensor {entity_name} wykryje zimno", + "connected": "pod\u0142\u0105czenie {entity_name}", + "gas": "sensor {entity_name} wykryje gaz", + "hot": "sensor {entity_name} wykryje gor\u0105co", + "light": "sensor {entity_name} wykryje \u015bwiat\u0142o", + "locked": "zamkni\u0119cie {entity_name}", + "moist\u00a7": "sensor {entity_name} wykryje wilgo\u0107", + "motion": "sensor {entity_name} wykryje ruch", + "moving": "sensor {entity_name} zacznie porusza\u0107 si\u0119", + "no_gas": "sensor {entity_name} przestanie wykrywa\u0107 gaz", + "no_light": "sensor {entity_name} przestanie wykrywa\u0107 \u015bwiat\u0142o", + "no_motion": "sensor {entity_name} przestanie wykrywa\u0107 ruch", + "no_problem": "sensor {entity_name} przestanie wykrywa\u0107 problem", + "no_smoke": "sensor {entity_name} przestanie wykrywa\u0107 dym", + "no_sound": "sensor {entity_name} przestanie wykrywa\u0107 d\u017awi\u0119k", + "no_vibration": "sensor {entity_name} przestanie wykrywa\u0107 wibracje", + "not_bat_low": "bateria {entity_name} staje si\u0119 na\u0142adowana", + "not_cold": "sensor {entity_name} przestanie wykrywa\u0107 zimno", + "not_connected": "roz\u0142\u0105czenie {entity_name}", + "not_hot": "sensor {entity_name} przestanie wykrywa\u0107 gor\u0105co", + "not_locked": "otwarcie {entity_name}", + "not_moist": "sensor {entity_name} przestanie wykrywa\u0107 wilgo\u0107", + "not_moving": "sensor {entity_name} przestanie porusza\u0107 si\u0119", + "not_occupied": "sensor {entity_name} przesta\u0142 by\u0107 zaj\u0119ty", + "not_plugged_in": "od\u0142\u0105czenie {entity_name}", + "not_powered": "od\u0142\u0105czenie zasilania {entity_name}", + "not_present": "sensor {entity_name} przestanie wykrywa\u0107 obecno\u015b\u0107", + "not_unsafe": "sensor {entity_name} przestanie wykrywa\u0107 niebezpiecze\u0144stwo", + "occupied": "sensor {entity_name} sta\u0142 si\u0119 zaj\u0119ty", + "opened": "otwarcie {entity_name}", + "plugged_in": "pod\u0142\u0105czenie {entity_name}", + "powered": "pod\u0142\u0105czenie zasilenia {entity_name}", + "present": "sensor {entity_name} wykryje obecno\u015b\u0107", + "problem": "sensor {entity_name} wykryje problem", + "smoke": "sensor {entity_name} wykryje dym", + "sound": "sensor {entity_name} wykryje d\u017awi\u0119k", + "turned_off": "wy\u0142\u0105czenie {entity_name}", + "turned_on": "w\u0142\u0105czenie {entity_name}", + "unsafe": "sensor {entity_name} wykryje niebezpiecze\u0144stwo", + "vibration": "sensor {entity_name} wykryje wibracje" } } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/pt-BR.json b/homeassistant/components/cert_expiry/.translations/pt-BR.json new file mode 100644 index 00000000000000..d26f0f9470e7e0 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "host_port_exists": "Essa combina\u00e7\u00e3o de host e porta j\u00e1 est\u00e1 configurada" + }, + "error": { + "certificate_fetch_failed": "N\u00e3o \u00e9 poss\u00edvel buscar o certificado dessa combina\u00e7\u00e3o de host e porta", + "connection_timeout": "Tempo limite ao conectar-se a este host", + "resolve_failed": "Este host n\u00e3o pode ser resolvido" + }, + "step": { + "user": { + "data": { + "host": "O nome do host do certificado", + "name": "O nome do certificado", + "port": "A porta do certificado" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index d92f318f61fda7..11a1beb10d6fa1 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -43,31 +43,31 @@ }, "device_automation": { "trigger_subtype": { - "both_buttons": "Oba przyciski", + "both_buttons": "oba przyciski", "button_1": "pierwszy przycisk", "button_2": "drugi przycisk", "button_3": "trzeci przycisk", "button_4": "czwarty przycisk", - "close": "Zamkni\u0119cie", - "dim_down": "Przyciemnienie", - "dim_up": "Przyciemnienie", + "close": "zamkni\u0119cie", + "dim_down": "zmniejszenie jasno\u015bci", + "dim_up": "zwi\u0119kszenie jasno\u015bci", "left": "w lewo", - "open": "Otwarcie", + "open": "otwarcie", "right": "w prawo", - "turn_off": "Wy\u0142\u0105czenie", - "turn_on": "W\u0142\u0105czenie" + "turn_off": "wy\u0142\u0105czenie", + "turn_on": "wy\u0142\u0105czenie" }, "trigger_type": { - "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "Przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "Przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_rotated": "Przycisk obr\u00f3cony \"{subtype}\"", - "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty", - "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony", - "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", - "remote_gyro_activated": "Potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" + "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_rotated": "przycisk obr\u00f3cony \"{subtype}\"", + "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", + "remote_gyro_activated": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" } }, "options": { diff --git a/homeassistant/components/dialogflow/.translations/es.json b/homeassistant/components/dialogflow/.translations/es.json index 1d6a849f3a8701..c106543e158b47 100644 --- a/homeassistant/components/dialogflow/.translations/es.json +++ b/homeassistant/components/dialogflow/.translations/es.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Solo una instancia es necesaria." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant, necesitas configurar [Integracion de flujos de dialogo de webhook]({dialogflow_url}).\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nVer [Documentaci\u00f3n]({docs_url}) para mas detalles." + "default": "Para enviar eventos a Home Assistant, necesitas configurar [Integraci\u00f3n de flujos de dialogo de webhook]({dialogflow_url}).\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nVer [Documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, "step": { "user": { diff --git a/homeassistant/components/ecobee/.translations/ca.json b/homeassistant/components/ecobee/.translations/ca.json new file mode 100644 index 00000000000000..2c4d16b5787cbf --- /dev/null +++ b/homeassistant/components/ecobee/.translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Aquesta integraci\u00f3 nom\u00e9s admet una sola inst\u00e0ncia ecobee." + }, + "error": { + "pin_request_failed": "Error al sol\u00b7licitar els PIN d'ecobee; verifica que la clau API \u00e9s correcta.", + "token_request_failed": "Error al sol\u00b7licitar els testimonis d'autenticaci\u00f3 d'ecobee; torna-ho a provar." + }, + "step": { + "authorize": { + "description": "Autoritza aquesta aplicaci\u00f3 a https://www.ecobee.com/consumerportal/index.html amb el codi pin seg\u00fcent: \n\n {pin} \n \n A continuaci\u00f3, prem Enviar.", + "title": "Autoritzaci\u00f3 de l'aplicaci\u00f3 a ecobee.com" + }, + "user": { + "data": { + "api_key": "Clau API" + }, + "description": "Introdueix la clau API obteinguda a trav\u00e9s del lloc web ecobee.com.", + "title": "Clau API d'ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/es.json b/homeassistant/components/ecobee/.translations/es.json new file mode 100644 index 00000000000000..5544d2e7f7bfdb --- /dev/null +++ b/homeassistant/components/ecobee/.translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Esta integraci\u00f3n actualmente solo admite una instancia de ecobee." + }, + "error": { + "pin_request_failed": "Error al solicitar el PIN de ecobee; verifique que la clave API sea correcta.", + "token_request_failed": "Error al solicitar tokens de ecobee; Int\u00e9ntalo de nuevo." + }, + "step": { + "authorize": { + "description": "Por favor, autorizar esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo pin:\n\n{pin}\n\nA continuaci\u00f3n, pulse Enviar.", + "title": "Autorizar aplicaci\u00f3n en ecobee.com" + }, + "user": { + "data": { + "api_key": "Clave API" + }, + "description": "Introduzca la clave de API obtenida de ecobee.com.", + "title": "Clave API de ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/sl.json b/homeassistant/components/ecobee/.translations/sl.json new file mode 100644 index 00000000000000..d70be59afb5d4d --- /dev/null +++ b/homeassistant/components/ecobee/.translations/sl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Ta integracija trenutno podpira samo en primerek ecobee." + }, + "error": { + "pin_request_failed": "Napaka pri zahtevi PIN-a od ecobee; preverite, ali je klju\u010d API pravilen.", + "token_request_failed": "Napaka pri zahtevanju \u017eetonov od ecobeeja; prosim poskusite ponovno." + }, + "step": { + "authorize": { + "description": "Prosimo, pooblastite to aplikacijo na https://www.ecobee.com/consumerportal/index.html s kodo PIN:\n\n{pin}\n\nNato pritisnite Po\u0161lji.", + "title": "Pooblasti aplikacijo na ecobee.com" + }, + "user": { + "data": { + "api_key": "API Klju\u010d" + }, + "description": "Prosimo vnesite API klju\u010d, pridobljen iz ecobee.com.", + "title": "ecobee API klju\u010d" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/es.json b/homeassistant/components/izone/.translations/es.json new file mode 100644 index 00000000000000..9f82b1a7b1fd73 --- /dev/null +++ b/homeassistant/components/izone/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se han encontrado dispositivos iZone en la red.", + "single_instance_allowed": "Solo es necesaria una \u00fanica configuraci\u00f3n de iZone." + }, + "step": { + "confirm": { + "description": "\u00bfQuieres configurar iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 4b649744ed977c..33a38fc930e403 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "Prze\u0142\u0105cz {entity_name}", - "turn_off": "Wy\u0142\u0105cz {entity_name}", - "turn_on": "W\u0142\u0105cz {entity_name}" + "toggle": "prze\u0142\u0105cz {entity_name}", + "turn_off": "wy\u0142\u0105cz {entity_name}", + "turn_on": "w\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "{entity_name} jest wy\u0142\u0105czony", - "is_on": "{entity_name} jest w\u0142\u0105czony" + "is_off": "\u015bwiat\u0142o {entity_name} jest wy\u0142\u0105czone", + "is_on": "\u015bwiat\u0142o {entity_name} jest w\u0142\u0105czone" }, "trigger_type": { - "turned_off": "Wy\u0142\u0105czenie {entity_name}", - "turned_on": "W\u0142\u0105czenie {entity_name}" + "turned_off": "wy\u0142\u0105czenie {entity_name}", + "turned_on": "w\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 4c24dddbe87917..14607868907960 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostra tots els controls", + "use_episode_art": "Utilitza imatges de l'episodi" + }, + "description": "Opcions per als reproductors multim\u00e8dia Plex" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json new file mode 100644 index 00000000000000..6d1ad1f62daa8c --- /dev/null +++ b/homeassistant/components/plex/.translations/es.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "all_configured": "Todos los servidores vinculados ya configurados", + "already_configured": "Este servidor Plex ya est\u00e1 configurado", + "already_in_progress": "Plex se est\u00e1 configurando", + "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "unknown": "Fall\u00f3 por razones desconocidas" + }, + "error": { + "faulty_credentials": "Error en la autorizaci\u00f3n", + "no_servers": "No hay servidores vinculados a la cuenta", + "no_token": "Proporcione un token o seleccione la configuraci\u00f3n manual", + "not_found": "No se ha encontrado el servidor Plex" + }, + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Puerto", + "ssl": "Usar SSL", + "token": "Token (es necesario)", + "verify_ssl": "Verificar certificado SSL" + }, + "title": "Servidor Plex" + }, + "select_server": { + "data": { + "server": "Servidor" + }, + "description": "Varios servidores disponibles, seleccione uno:", + "title": "Seleccione el servidor Plex" + }, + "user": { + "data": { + "manual_setup": "Configuraci\u00f3n manual", + "token": "Token Plex" + }, + "description": "Introduzca un token Plex para la configuraci\u00f3n autom\u00e1tica o configure manualmente un servidor.", + "title": "Conectar servidor Plex" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostrar todos los controles", + "use_episode_art": "Usar el arte de episodios" + }, + "description": "Opciones para reproductores multimedia Plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 2a03b7d0e8cd3d..49ed34baf763b2 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Poka\u017ei vse kontrole", + "use_episode_art": "Uporabi naslovno sliko epizode" + }, + "description": "Mo\u017enosti za predvajalnike Plex" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index 2cf3fa2c1a7e81..5f6d0c41c13506 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "\u986f\u793a\u6240\u6709\u63a7\u5236", + "use_episode_art": "\u4f7f\u7528\u5f71\u96c6\u5287\u7167" + }, + "description": "Plex \u64ad\u653e\u5668\u9078\u9805" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index 201a77a76a5d09..09b43f4100d660 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -1,19 +1,19 @@ { "device_automation": { "action_type": { - "toggle": "Prze\u0142\u0105cz {entity_name}", - "turn_off": "Wy\u0142\u0105cz {entity_name}", - "turn_on": "W\u0142\u0105cz {entity_name}" + "toggle": "prze\u0142\u0105cz {entity_name}", + "turn_off": "wy\u0142\u0105cz {entity_name}", + "turn_on": "w\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "{entity_name} jest wy\u0142\u0105czony", - "is_on": "{entity_name} jest w\u0142\u0105czony", - "turn_off": "{entity_name} wy\u0142\u0105czony", - "turn_on": "{entity_name} w\u0142\u0105czony" + "is_off": "prze\u0142\u0105cznik {entity_name} jest wy\u0142\u0105czony", + "is_on": "prze\u0142\u0105cznik {entity_name} jest w\u0142\u0105czony", + "turn_off": "prze\u0142\u0105cznik {entity_name} wy\u0142\u0105czony", + "turn_on": "prze\u0142\u0105cznik {entity_name} w\u0142\u0105czony" }, "trigger_type": { - "turned_off": "Wy\u0142\u0105czenie {entity_name}", - "turned_on": "W\u0142\u0105czenie {entity_name}" + "turned_off": "wy\u0142\u0105czenie {entity_name}", + "turned_on": "w\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/pt-BR.json b/homeassistant/components/traccar/.translations/pt-BR.json new file mode 100644 index 00000000000000..9fc23b3e394074 --- /dev/null +++ b/homeassistant/components/traccar/.translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/ca.json b/homeassistant/components/transmission/.translations/ca.json new file mode 100644 index 00000000000000..395f6e2d6810c9 --- /dev/null +++ b/homeassistant/components/transmission/.translations/ca.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "error": { + "cannot_connect": "No s'ha pogut connectar amb l'amfitri\u00f3", + "wrong_credentials": "Nom d'usuari o contrasenya incorrectes" + }, + "step": { + "options": { + "data": { + "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + }, + "title": "Opcions de configuraci\u00f3" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + }, + "title": "Configuraci\u00f3 del client de Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + }, + "description": "Opcions de configuraci\u00f3 per a Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/es.json b/homeassistant/components/transmission/.translations/es.json new file mode 100644 index 00000000000000..f210eb42f8a613 --- /dev/null +++ b/homeassistant/components/transmission/.translations/es.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "S\u00f3lo se necesita una sola instancia." + }, + "error": { + "cannot_connect": "No se puede conectar al host", + "wrong_credentials": "Nombre de usuario o contrase\u00f1a incorrectos" + }, + "step": { + "options": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n" + }, + "title": "Configurar opciones" + }, + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + }, + "title": "Configuraci\u00f3n del cliente de transmisi\u00f3n" + } + }, + "title": "Transmisi\u00f3n" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n" + }, + "description": "Configurar opciones para la transmisi\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pt-BR.json b/homeassistant/components/transmission/.translations/pt-BR.json new file mode 100644 index 00000000000000..a2d3d177e22d9f --- /dev/null +++ b/homeassistant/components/transmission/.translations/pt-BR.json @@ -0,0 +1,37 @@ +{ + "config": { + "error": { + "cannot_connect": "N\u00e3o foi poss\u00edvel conectar ao host", + "wrong_credentials": "Nome de usu\u00e1rio ou senha incorretos" + }, + "step": { + "options": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" + }, + "title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o" + }, + "user": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "title": "Configurar o cliente de transmiss\u00e3o" + } + }, + "title": "Transmiss\u00e3o" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" + }, + "description": "Configurar op\u00e7\u00f5es para transmiss\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/sl.json b/homeassistant/components/transmission/.translations/sl.json new file mode 100644 index 00000000000000..122c332f429d84 --- /dev/null +++ b/homeassistant/components/transmission/.translations/sl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "error": { + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z gostiteljem", + "wrong_credentials": "Napa\u010dno uporabni\u0161ko ime ali geslo" + }, + "step": { + "options": { + "data": { + "scan_interval": "Pogostost posodabljanja" + }, + "title": "Nastavite mo\u017enosti" + }, + "user": { + "data": { + "host": "Gostitelj", + "name": "Ime", + "password": "Geslo", + "port": "Vrata", + "username": "Uporabni\u0161ko ime" + }, + "title": "Namestitev odjemalca Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Pogostost posodabljanja" + }, + "description": "Nastavite mo\u017enosti za Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/zh-Hant.json b/homeassistant/components/transmission/.translations/zh-Hant.json new file mode 100644 index 00000000000000..479e25c6d8a124 --- /dev/null +++ b/homeassistant/components/transmission/.translations/zh-Hant.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef", + "wrong_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4" + }, + "step": { + "options": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387" + }, + "title": "\u8a2d\u5b9a\u9078\u9805" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u8a2d\u5b9a Transmission \u5ba2\u6236\u7aef" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387" + }, + "description": "Transmission \u8a2d\u5b9a\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/pt-BR.json b/homeassistant/components/unifi/.translations/pt-BR.json index a7eac61bab399d..ea13035e09bec1 100644 --- a/homeassistant/components/unifi/.translations/pt-BR.json +++ b/homeassistant/components/unifi/.translations/pt-BR.json @@ -22,5 +22,15 @@ } }, "title": "Controlador UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "track_clients": "Rastrear clientes da rede", + "track_wired_clients": "Incluir clientes de rede com fio" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/es.json b/homeassistant/components/withings/.translations/es.json index fac325a7097645..ee0cf5235884c2 100644 --- a/homeassistant/components/withings/.translations/es.json +++ b/homeassistant/components/withings/.translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Debe configurar Withings antes de poder autenticarse con \u00e9l. Por favor, lea la documentaci\u00f3n." + }, "create_entry": { "default": "Autenticado correctamente con Withings para el perfil seleccionado." }, diff --git a/homeassistant/components/zha/.translations/ca.json b/homeassistant/components/zha/.translations/ca.json index 635d0ecbde2f9b..2b8230ad689109 100644 --- a/homeassistant/components/zha/.translations/ca.json +++ b/homeassistant/components/zha/.translations/ca.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Av\u00eds" + }, + "trigger_subtype": { + "both_buttons": "Ambd\u00f3s botons", + "button_1": "Primer bot\u00f3", + "button_2": "Segon bot\u00f3", + "button_3": "Tercer bot\u00f3", + "button_4": "Quart bot\u00f3", + "button_5": "Cinqu\u00e8 bot\u00f3", + "button_6": "Sis\u00e8 bot\u00f3", + "close": "Tanca", + "dim_down": "Atenua la brillantor", + "dim_up": "Augmenta la brillantor", + "face_1": "amb la cara 1 activada", + "face_2": "amb la cara 2 activada", + "face_3": "amb la cara 3 activada", + "face_4": "amb la cara 4 activada", + "face_5": "amb la cara 5 activada", + "face_6": "amb la cara 6 activada", + "face_any": "Amb qualsevol o alguna de les cares especificades activades.", + "left": "Esquerra", + "open": "Obert", + "right": "Dreta", + "turn_off": "Desactiva", + "turn_on": "Activa" + }, + "trigger_type": { + "device_dropped": "Dispositiu caigut", + "device_flipped": "Dispositiu voltejat a \"{subtype}\"", + "device_knocked": "Dispositiu colpejat a \"{subtype}\"", + "device_rotated": "Dispositiu rotat a \"{subtype}\"", + "device_shaken": "Dispositiu sacsejat", + "device_slid": "Dispositiu lliscat a \"{subtype}\"", + "device_tilted": "Dispositiu inclinat", + "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives", + "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament", + "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives", + "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives", + "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", + "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", + "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/es.json b/homeassistant/components/zha/.translations/es.json index 0047c762a9de0d..b8529ce9047376 100644 --- a/homeassistant/components/zha/.translations/es.json +++ b/homeassistant/components/zha/.translations/es.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Advertir" + }, + "trigger_subtype": { + "both_buttons": "Ambos botones", + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "button_5": "Quinto bot\u00f3n", + "button_6": "Sexto bot\u00f3n", + "close": "Cerrar", + "dim_down": "Bajar la intensidad", + "dim_up": "Subir la intensidad", + "face_1": "con la cara 1 activada", + "face_2": "con la cara 2 activada", + "face_3": "con la cara 3 activada", + "face_4": "con la cara 4 activada", + "face_5": "con la cara 5 activada", + "face_6": "con la cara 6 activada", + "face_any": "Con cualquier cara/especificada(s) activada(s)", + "left": "Izquierda", + "open": "Abrir", + "right": "Derecha", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "device_dropped": "Dispositivo ca\u00eddo", + "device_flipped": "Dispositivo volteado \" {subtype} \"", + "device_knocked": "Dispositivo eliminado \" {subtype} \"", + "device_rotated": "Dispositivo girado \" {subtype} \"", + "device_shaken": "Dispositivo agitado", + "device_slid": "Dispositivo deslizado \" {subtype} \"", + "device_tilted": "Dispositivo inclinado", + "remote_button_double_press": "\"{subtipo}\" bot\u00f3n de doble clic", + "remote_button_long_press": "Bot\u00f3n \"{subtipo}\" pulsado continuamente", + "remote_button_long_release": "Bot\u00f3n \"{subtipo}\" liberado despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_button_quadruple_press": "\"{subtipo}\" bot\u00f3n cu\u00e1druple pulsado", + "remote_button_quintuple_press": "\"{subtipo}\" bot\u00f3n qu\u00edntuple pulsado", + "remote_button_short_press": "Bot\u00f3n \"{subtipo}\" pulsado", + "remote_button_short_release": "Bot\u00f3n \"{subtipo}\" liberado", + "remote_button_triple_press": "\"{subtipo}\" bot\u00f3n de triple clic" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pl.json b/homeassistant/components/zha/.translations/pl.json index 76f1c58fe7ccc8..0e1b7028dbbd14 100644 --- a/homeassistant/components/zha/.translations/pl.json +++ b/homeassistant/components/zha/.translations/pl.json @@ -19,20 +19,20 @@ }, "device_automation": { "action_type": { - "squawk": "Skrzek", - "warn": "Ostrze\u017cenie" + "squawk": "squawk", + "warn": "ostrze\u017cenie" }, "trigger_subtype": { - "both_buttons": "Oba przyciski", - "button_1": "Pierwszy przycisk", - "button_2": "Drugi przycisk", - "button_3": "Trzeci przycisk", - "button_4": "Czwarty przycisk", - "button_5": "Pi\u0105ty przycisk", - "button_6": "Sz\u00f3sty przycisk", - "close": "Zamkni\u0119cie", - "dim_down": "\u015aciemnianie", - "dim_up": "Rozja\u015bnienie", + "both_buttons": "oba przyciski", + "button_1": "pierwszy przycisk", + "button_2": "drugi przycisk", + "button_3": "trzeci przycisk", + "button_4": "czwarty przycisk", + "button_5": "pi\u0105ty przycisk", + "button_6": "sz\u00f3sty przycisk", + "close": "zamkni\u0119cie", + "dim_down": "zmniejszenie jasno\u015bci", + "dim_up": "zwi\u0119kszenie jasno\u015bci", "face_1": "z aktywowan\u0105 twarz\u0105 1", "face_2": "z aktywowan\u0105 twarz\u0105 2", "face_3": "z aktywowan\u0105 twarz\u0105 3", @@ -41,27 +41,27 @@ "face_6": "z aktywowan\u0105 twarz\u0105 6", "face_any": "z dowoln\u0105 twarz\u0105 aktywowan\u0105", "left": "w lewo", - "open": "Otwarcie", + "open": "otwarcie", "right": "w prawo", - "turn_off": "Wy\u0142\u0105czenie", - "turn_on": "W\u0142\u0105czenie" + "turn_off": "wy\u0142\u0105czenie", + "turn_on": "w\u0142\u0105czenie" }, "trigger_type": { - "device_dropped": "Upadek urz\u0105dzenia", - "device_flipped": "Odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", - "device_knocked": "Pukni\u0119cie urz\u0105dzenia \"{subtype}\"", - "device_rotated": "Obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", - "device_shaken": "Potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", - "device_slid": "Przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", - "device_tilted": "Przechylenie urz\u0105dzenia", - "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "Przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "Przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty", - "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony", - "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty" + "device_dropped": "upadek urz\u0105dzenia", + "device_flipped": "odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_knocked": "pukni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_rotated": "obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_shaken": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", + "device_slid": "przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_tilted": "przechylenie urz\u0105dzenia", + "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pt-BR.json b/homeassistant/components/zha/.translations/pt-BR.json index 8606a04e197985..0bc3afe28eca5d 100644 --- a/homeassistant/components/zha/.translations/pt-BR.json +++ b/homeassistant/components/zha/.translations/pt-BR.json @@ -16,5 +16,10 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "warn": "Aviso" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/sl.json b/homeassistant/components/zha/.translations/sl.json index 30df6716f97fda..226bd37200efb6 100644 --- a/homeassistant/components/zha/.translations/sl.json +++ b/homeassistant/components/zha/.translations/sl.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Opozori" + }, + "trigger_subtype": { + "both_buttons": "Oba gumba", + "button_1": "Prvi gumb", + "button_2": "Drugi gumb", + "button_3": "Tretji gumb", + "button_4": "\u010cetrti gumb", + "button_5": "Peti gumb", + "button_6": "\u0160esti gumb", + "close": "Zapri", + "dim_down": "Zatemnite", + "dim_up": "pove\u010dajte mo\u010d", + "face_1": "aktivirano z obrazom 1", + "face_2": "aktivirano z obrazom 2", + "face_3": "aktivirano z obrazom 3", + "face_4": "aktivirano z obrazom 4", + "face_5": "aktivirano z obrazom 5", + "face_6": "aktivirano z obrazom 6", + "face_any": "Z vsemi/dolo\u010denimi obrazi vklju\u010deno", + "left": "Levo", + "open": "Odprto", + "right": "Desno", + "turn_off": "Ugasni", + "turn_on": "Pri\u017egi" + }, + "trigger_type": { + "device_dropped": "Naprava padla", + "device_flipped": "Naprava obrnjena \"{subtype}\"", + "device_knocked": "Naprava prevrnjena \"{subtype}\"", + "device_rotated": "Naprava je zasukana \"{subtype}\"", + "device_shaken": "Naprava se je pretresla", + "device_slid": "Naprava zdrsnila \"{subtype}\"", + "device_tilted": "Naprava je nagnjena", + "remote_button_double_press": "Dvakrat kliknete gumb \"{subtype}\"", + "remote_button_long_press": "\"{subtype}\" gumb neprekinjeno pritisnjen", + "remote_button_long_release": "\"{subtype}\" gumb spro\u0161\u010den po dolgem pritisku", + "remote_button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen", + "remote_button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen", + "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", + "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", + "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/zh-Hant.json b/homeassistant/components/zha/.translations/zh-Hant.json index bbfb3fe7128f27..d7f421c7e84498 100644 --- a/homeassistant/components/zha/.translations/zh-Hant.json +++ b/homeassistant/components/zha/.translations/zh-Hant.json @@ -18,6 +18,10 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "\u61c9\u7b54", + "warn": "\u8b66\u544a" + }, "trigger_subtype": { "both_buttons": "\u5169\u500b\u6309\u9215", "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", From 5bd3d4aa0bf7829114d8932beeae7587583c975b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 29 Sep 2019 20:33:42 -0400 Subject: [PATCH 216/296] Bump zha quirks to 0.0.26 (#27051) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c4de1d66e838b7..f0f6389a061e13 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.10.0", - "zha-quirks==0.0.25", + "zha-quirks==0.0.26", "zigpy-deconz==0.4.0", "zigpy-homeassistant==0.9.0", "zigpy-xbee-homeassistant==0.5.0", diff --git a/requirements_all.txt b/requirements_all.txt index 79313b48e61ed2..dadf60e166bdef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2014,7 +2014,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.25 +zha-quirks==0.0.26 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From 245e51df7a1e30dc602bff805417bbbe094abf09 Mon Sep 17 00:00:00 2001 From: John Luetke Date: Sun, 29 Sep 2019 17:35:56 -0700 Subject: [PATCH 217/296] Add Pi-hole enable and disable services (#27055) * Add service to disable pihole * Add service to enable pihole * Redefine optional string validator * code review changes * Change service parameter to timedelta * code review changes --- homeassistant/components/pi_hole/__init__.py | 44 ++++++++++++++++++- homeassistant/components/pi_hole/const.py | 4 ++ .../components/pi_hole/services.yaml | 8 ++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/pi_hole/services.yaml diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index ffc9827eed41fe..00bb0e2d673486 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -5,7 +5,13 @@ from hole import Hole from hole.exceptions import HoleError -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_API_KEY, + CONF_SSL, + CONF_VERIFY_SSL, +) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -21,6 +27,9 @@ DEFAULT_SSL, DEFAULT_VERIFY_SSL, MIN_TIME_BETWEEN_UPDATES, + SERVICE_DISABLE, + SERVICE_DISABLE_ATTR_DURATION, + SERVICE_ENABLE, ) LOGGER = logging.getLogger(__name__) @@ -31,6 +40,7 @@ { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_API_KEY): cv.string, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, @@ -40,6 +50,14 @@ extra=vol.ALLOW_EXTRA, ) +SERVICE_DISABLE_SCHEMA = vol.Schema( + { + vol.Required(SERVICE_DISABLE_ATTR_DURATION): vol.All( + cv.time_period_str, cv.positive_timedelta + ) + } +) + async def async_setup(hass, config): """Set up the pi_hole integration.""" @@ -50,6 +68,7 @@ async def async_setup(hass, config): use_tls = conf[CONF_SSL] verify_tls = conf[CONF_VERIFY_SSL] location = conf[CONF_LOCATION] + api_key = conf.get(CONF_API_KEY) LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) @@ -62,6 +81,7 @@ async def async_setup(hass, config): location=location, tls=use_tls, verify_tls=verify_tls, + api_token=api_key, ), name, ) @@ -70,6 +90,28 @@ async def async_setup(hass, config): hass.data[DOMAIN] = pi_hole + async def handle_disable(call): + if api_key is None: + raise vol.Invalid("Pi-hole api_key must be provided in configuration") + + duration = call.data[SERVICE_DISABLE_ATTR_DURATION].total_seconds() + + LOGGER.debug("Disabling %s %s for %d seconds", DOMAIN, host, duration) + await pi_hole.api.disable(duration) + + async def handle_enable(call): + if api_key is None: + raise vol.Invalid("Pi-hole api_key must be provided in configuration") + + LOGGER.debug("Enabling %s %s", DOMAIN, host) + await pi_hole.api.enable() + + hass.services.async_register( + DOMAIN, SERVICE_DISABLE, handle_disable, schema=SERVICE_DISABLE_SCHEMA + ) + + hass.services.async_register(DOMAIN, SERVICE_ENABLE, handle_enable) + hass.async_create_task(async_load_platform(hass, SENSOR_DOMAIN, DOMAIN, {}, config)) return True diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index ba83bf1d805334..5422054795097e 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -12,6 +12,10 @@ DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True +SERVICE_DISABLE = "disable" +SERVICE_ENABLE = "enable" +SERVICE_DISABLE_ATTR_DURATION = "duration" + ATTR_BLOCKED_DOMAINS = "domains_blocked" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) diff --git a/homeassistant/components/pi_hole/services.yaml b/homeassistant/components/pi_hole/services.yaml new file mode 100644 index 00000000000000..b16ed21a5d3c2b --- /dev/null +++ b/homeassistant/components/pi_hole/services.yaml @@ -0,0 +1,8 @@ +disable: + description: Disable Pi-hole for an amount of time + fields: + duration: + description: Time that the Pi-hole should be disabled for + example: "00:00:15" +enable: + description: Enable Pi-hole \ No newline at end of file From 43bd1168521c9296658f9df480161c0966b87fe9 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Mon, 30 Sep 2019 02:56:02 -0400 Subject: [PATCH 218/296] add utc tz to forecast (#27049) --- homeassistant/components/darksky/weather.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index e95381cdf73c61..5296f34662618e 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -1,5 +1,5 @@ """Support for retrieving meteorological data from Dark Sky.""" -from datetime import datetime, timedelta +from datetime import timedelta import logging from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout @@ -29,6 +29,7 @@ ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle +from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.pressure import convert as convert_pressure _LOGGER = logging.getLogger(__name__) @@ -178,7 +179,7 @@ def calc_precipitation(intensity, hours): if self._mode == "daily": data = [ { - ATTR_FORECAST_TIME: datetime.fromtimestamp( + ATTR_FORECAST_TIME: utc_from_timestamp( entry.d.get("time") ).isoformat(), ATTR_FORECAST_TEMP: entry.d.get("temperatureHigh"), @@ -195,7 +196,7 @@ def calc_precipitation(intensity, hours): else: data = [ { - ATTR_FORECAST_TIME: datetime.fromtimestamp( + ATTR_FORECAST_TIME: utc_from_timestamp( entry.d.get("time") ).isoformat(), ATTR_FORECAST_TEMP: entry.d.get("temperature"), From c527e0f16440b9592dbf6c7af4f2a8623f83d8fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20K=C3=BCgler?= Date: Mon, 30 Sep 2019 09:06:10 +0200 Subject: [PATCH 219/296] Fix rest_command when server is unreachable (#26948) * fix rest_command when server is unreachable When a server doesn't exist, the connection fails immediately, rather than waiting for a timeout. This means that the async handler is never reached, and the request variable never filled, yet it's used in the client error exception handler, so this one bugs out. By using the command_config, we avoid using the potentially unassigned request variable, avoiding this problem. This patch makes scripts work that have a rest_command in them which fails due to a server being offline. * render template_url instead of printing the template object * fix formatting * fix format using black * only render url once * blacken... --- homeassistant/components/rest_command/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 038787ac8d4579..1607000e8d985a 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -94,13 +94,11 @@ async def async_service_handler(service): template_payload.async_render(variables=service.data), "utf-8" ) + request_url = template_url.async_render(variables=service.data) try: with async_timeout.timeout(timeout): request = await getattr(websession, method)( - template_url.async_render(variables=service.data), - data=payload, - auth=auth, - headers=headers, + request_url, data=payload, auth=auth, headers=headers ) if request.status < 400: @@ -112,7 +110,7 @@ async def async_service_handler(service): _LOGGER.warning("Timeout call %s.", request.url) except aiohttp.ClientError: - _LOGGER.error("Client error %s.", request.url) + _LOGGER.error("Client error %s.", request_url) # register services hass.services.async_register(DOMAIN, name, async_service_handler) From fa92d0e6d8a8539975012b9e7c2010fd0251011d Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 30 Sep 2019 09:31:35 +0100 Subject: [PATCH 220/296] Fix incomfort and Bump client to 0.3.5 (#26802) * remove superfluous device state attributes * fix water_heater icon * add type hints * fix issue #26760 * bump client to v0.3.5 * add unique_id --- .../components/incomfort/binary_sensor.py | 32 ++++--- homeassistant/components/incomfort/climate.py | 24 +++-- .../components/incomfort/manifest.json | 2 +- homeassistant/components/incomfort/sensor.py | 89 +++++++++++-------- .../components/incomfort/water_heater.py | 72 +++++++-------- requirements_all.txt | 2 +- 6 files changed, 124 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 004086ab5c825c..39a45429cb1d15 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -1,4 +1,6 @@ -"""Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" +"""Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" +from typing import Any, Dict, Optional + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -8,6 +10,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an InComfort/InTouch binary_sensor device.""" + if discovery_info is None: + return + async_add_entities( [IncomfortFailed(hass.data[DOMAIN]["client"], hass.data[DOMAIN]["heater"])] ) @@ -16,33 +21,40 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class IncomfortFailed(BinarySensorDevice): """Representation of an InComfort Failed sensor.""" - def __init__(self, client, boiler): + def __init__(self, client, heater) -> None: """Initialize the binary sensor.""" + self._unique_id = f"{heater.serial_no}_failed" + self._client = client - self._boiler = boiler + self._heater = heater - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._refresh) @callback - def _refresh(self): + def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) @property - def name(self): + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> Optional[str]: """Return the name of the sensor.""" return "Fault state" @property - def is_on(self): + def is_on(self) -> bool: """Return the status of the sensor.""" - return self._boiler.status["is_failed"] + return self._heater.status["is_failed"] @property - def device_state_attributes(self): + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the device state attributes.""" - return {"fault_code": self._boiler.status["fault_code"]} + return {"fault_code": self._heater.status["fault_code"]} @property def should_poll(self) -> bool: diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index cccb9d256444dc..3918244d4e856d 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -1,5 +1,5 @@ """Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" -from typing import Any, Dict, Optional, List +from typing import Any, Dict, List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -13,21 +13,24 @@ from . import DOMAIN -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an InComfort/InTouch climate device.""" + if discovery_info is None: + return + client = hass.data[DOMAIN]["client"] heater = hass.data[DOMAIN]["heater"] - async_add_entities([InComfortClimate(client, r) for r in heater.rooms]) + async_add_entities([InComfortClimate(client, heater, r) for r in heater.rooms]) class InComfortClimate(ClimateDevice): """Representation of an InComfort/InTouch climate device.""" - def __init__(self, client, room): + def __init__(self, client, heater, room) -> None: """Initialize the climate device.""" + self._unique_id = f"{heater.serial_no}_{room.room_no}" + self._client = client self._room = room self._name = f"Room {room.room_no}" @@ -37,7 +40,7 @@ async def async_added_to_hass(self) -> None: async_dispatcher_connect(self.hass, DOMAIN, self._refresh) @callback - def _refresh(self): + def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) @property @@ -45,6 +48,11 @@ def should_poll(self) -> bool: """Return False as this device should never be polled.""" return False + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + @property def name(self) -> str: """Return the name of the climate device.""" @@ -78,7 +86,7 @@ def current_temperature(self) -> Optional[float]: @property def target_temperature(self) -> Optional[float]: """Return the temperature we try to reach.""" - return self._room.override + return self._room.setpoint @property def supported_features(self) -> int: diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 8b5f461c8afb08..c26ba27a29ae42 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -3,7 +3,7 @@ "name": "Intergas InComfort/Intouch Lan2RF gateway", "documentation": "https://www.home-assistant.io/components/incomfort", "requirements": [ - "incomfort-client==0.3.1" + "incomfort-client==0.3.5" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index e19bbf42aeacbc..772b5dab1834a1 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -1,31 +1,43 @@ -"""Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" -from homeassistant.const import PRESSURE_BAR, TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE +"""Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" +from typing import Any, Dict, Optional + +from homeassistant.const import ( + PRESSURE_BAR, + TEMP_CELSIUS, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify from . import DOMAIN -INTOUCH_HEATER_TEMP = "CV Temp" -INTOUCH_PRESSURE = "CV Pressure" -INTOUCH_TAP_TEMP = "Tap Temp" +INCOMFORT_HEATER_TEMP = "CV Temp" +INCOMFORT_PRESSURE = "CV Pressure" +INCOMFORT_TAP_TEMP = "Tap Temp" -INTOUCH_MAP_ATTRS = { - INTOUCH_HEATER_TEMP: ["heater_temp", "is_pumping"], - INTOUCH_TAP_TEMP: ["tap_temp", "is_tapping"], +INCOMFORT_MAP_ATTRS = { + INCOMFORT_HEATER_TEMP: ["heater_temp", "is_pumping"], + INCOMFORT_PRESSURE: ["pressure", None], + INCOMFORT_TAP_TEMP: ["tap_temp", "is_tapping"], } async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an InComfort/InTouch sensor device.""" + if discovery_info is None: + return + client = hass.data[DOMAIN]["client"] heater = hass.data[DOMAIN]["heater"] async_add_entities( [ - IncomfortPressure(client, heater, INTOUCH_PRESSURE), - IncomfortTemperature(client, heater, INTOUCH_HEATER_TEMP), - IncomfortTemperature(client, heater, INTOUCH_TAP_TEMP), + IncomfortPressure(client, heater, INCOMFORT_PRESSURE), + IncomfortTemperature(client, heater, INCOMFORT_HEATER_TEMP), + IncomfortTemperature(client, heater, INCOMFORT_TAP_TEMP), ] ) @@ -33,35 +45,47 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class IncomfortSensor(Entity): """Representation of an InComfort/InTouch sensor device.""" - def __init__(self, client, boiler): + def __init__(self, client, heater, name) -> None: """Initialize the sensor.""" self._client = client - self._boiler = boiler + self._heater = heater + + self._unique_id = f"{heater.serial_no}_{slugify(name)}" - self._name = None + self._name = name self._device_class = None self._unit_of_measurement = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._refresh) @callback - def _refresh(self): + def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) @property - def name(self): + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> Optional[str]: """Return the name of the sensor.""" return self._name @property - def device_class(self): + def state(self) -> Optional[str]: + """Return the state of the sensor.""" + return self._heater.status[INCOMFORT_MAP_ATTRS[self._name][0]] + + @property + def device_class(self) -> Optional[str]: """Return the device class of the sensor.""" return self._device_class @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> Optional[str]: """Return the unit of measurement of the sensor.""" return self._unit_of_measurement @@ -74,37 +98,26 @@ def should_poll(self) -> bool: class IncomfortPressure(IncomfortSensor): """Representation of an InTouch CV Pressure sensor.""" - def __init__(self, client, boiler, name): + def __init__(self, client, heater, name) -> None: """Initialize the sensor.""" - super().__init__(client, boiler) + super().__init__(client, heater, name) - self._name = name + self._device_class = DEVICE_CLASS_PRESSURE self._unit_of_measurement = PRESSURE_BAR - @property - def state(self): - """Return the state/value of the sensor.""" - return self._boiler.status["pressure"] - class IncomfortTemperature(IncomfortSensor): """Representation of an InTouch Temperature sensor.""" - def __init__(self, client, boiler, name): + def __init__(self, client, heater, name) -> None: """Initialize the signal strength sensor.""" - super().__init__(client, boiler) + super().__init__(client, heater, name) - self._name = name self._device_class = DEVICE_CLASS_TEMPERATURE self._unit_of_measurement = TEMP_CELSIUS @property - def state(self): - """Return the state of the sensor.""" - return self._boiler.status[INTOUCH_MAP_ATTRS[self._name][0]] - - @property - def device_state_attributes(self): + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the device state attributes.""" - key = INTOUCH_MAP_ATTRS[self._name][1] - return {key: self._boiler.status[key]} + key = INCOMFORT_MAP_ATTRS[self._name][1] + return {key: self._heater.status[key]} diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 2449a1223ccc5f..70423611705d20 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -1,6 +1,7 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" import asyncio import logging +from typing import Any, Dict, Optional from aiohttp import ClientResponseError from homeassistant.components.water_heater import WaterHeaterDevice @@ -11,60 +12,52 @@ _LOGGER = logging.getLogger(__name__) -HEATER_SUPPORT_FLAGS = 0 +HEATER_ATTRS = ["display_code", "display_text", "is_burning"] -HEATER_MAX_TEMP = 80.0 -HEATER_MIN_TEMP = 30.0 -HEATER_NAME = "Boiler" -HEATER_ATTRS = [ - "display_code", - "display_text", - "is_burning", - "rf_message_rssi", - "nodenr", - "rfstatus_cntr", -] - - -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an InComfort/Intouch water_heater device.""" + if discovery_info is None: + return + client = hass.data[DOMAIN]["client"] heater = hass.data[DOMAIN]["heater"] - async_add_entities([IncomfortWaterHeater(client, heater)], update_before_add=True) + async_add_entities([IncomfortWaterHeater(client, heater)]) class IncomfortWaterHeater(WaterHeaterDevice): """Representation of an InComfort/Intouch water_heater device.""" - def __init__(self, client, heater): + def __init__(self, client, heater) -> None: """Initialize the water_heater device.""" + self._unique_id = f"{heater.serial_no}" + self._client = client self._heater = heater @property - def name(self): + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> str: """Return the name of the water_heater device.""" - return HEATER_NAME + return "Boiler" @property - def icon(self): + def icon(self) -> str: """Return the icon of the water_heater device.""" - return "mdi:oil-temperature" + return "mdi:thermometer-lines" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" - state = { - k: self._heater.status[k] for k in self._heater.status if k in HEATER_ATTRS - } - return state + return {k: v for k, v in self._heater.status.items() if k in HEATER_ATTRS} @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" if self._heater.is_tapping: return self._heater.tap_temp @@ -73,34 +66,34 @@ def current_temperature(self): return max(self._heater.heater_temp, self._heater.tap_temp) @property - def min_temp(self): + def min_temp(self) -> float: """Return max valid temperature that can be set.""" - return HEATER_MIN_TEMP + return 80.0 @property - def max_temp(self): + def max_temp(self) -> float: """Return max valid temperature that can be set.""" - return HEATER_MAX_TEMP + return 30.0 @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" - return HEATER_SUPPORT_FLAGS + return 0 @property - def current_operation(self): + def current_operation(self) -> str: """Return the current operation mode.""" if self._heater.is_failed: return f"Fault code: {self._heater.fault_code}" return self._heater.display_text - async def async_update(self): + async def async_update(self) -> None: """Get the latest state data from the gateway.""" try: await self._heater.update() @@ -108,4 +101,5 @@ async def async_update(self): except (ClientResponseError, asyncio.TimeoutError) as err: _LOGGER.warning("Update failed, message is: %s", err) - async_dispatcher_send(self.hass, DOMAIN) + else: + async_dispatcher_send(self.hass, DOMAIN) diff --git a/requirements_all.txt b/requirements_all.txt index dadf60e166bdef..c25ca6a54fe400 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -685,7 +685,7 @@ iglo==1.2.7 ihcsdk==2.3.0 # homeassistant.components.incomfort -incomfort-client==0.3.1 +incomfort-client==0.3.5 # homeassistant.components.influxdb influxdb==5.2.3 From 21453df73eca24f62ed449ce74a29756aaaa1cf0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 30 Sep 2019 11:01:08 +0200 Subject: [PATCH 221/296] Update devcontainer.json --- .devcontainer/devcontainer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e78a8e6851c728..afb273331aada5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,6 +8,7 @@ "runArgs": ["-e", "GIT_EDITOR=\"code --wait\""], "extensions": [ "ms-python.python", + "visualstudioexptteam.vscodeintellicode", "ms-azure-devops.azure-pipelines", "redhat.vscode-yaml", "esbenp.prettier-vscode" From 48d07467d913367c85338d6883dbc538cb359e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiit=20R=C3=A4tsep?= Date: Mon, 30 Sep 2019 15:23:08 +0300 Subject: [PATCH 222/296] Add support for SOMA Smartshades devices (#26226) * Add Soma integration * Fixed cover position get/set * Try to list devices before creating config entries to see if Soma Connect can be polled * Style fixes * Updated requirements * Updated .coveragerc to ignore Soma component * Fixed linter errors * Implemented stop command * Test coverage fixes according to feedback * Fixes to code according to feedback * Added error logging and tested config from yaml * Indentation fix * Removed unnecessary method * Wrong indentation * Added some tests * Added test for import step leading to entry creation * Added feedback to user form in case of connection error * Minor fixes according to feedback * Changed exception type in error handling for connection to Connect * To keep API consistent for Google Home and Alexa we swapped the open/closed position values back and I reversed them in this integration as well * regenerated requirements, ran black, addde __init__.py to ignore file * Added pysoma library to gen_requirements_all.py * Added missing test case * removed useless return value --- .coveragerc | 2 + CODEOWNERS | 1 + .../components/soma/.translations/en.json | 24 ++++ homeassistant/components/soma/__init__.py | 111 ++++++++++++++++++ homeassistant/components/soma/config_flow.py | 56 +++++++++ homeassistant/components/soma/const.py | 6 + homeassistant/components/soma/cover.py | 79 +++++++++++++ homeassistant/components/soma/manifest.json | 13 ++ homeassistant/components/soma/strings.json | 13 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/soma/__init__.py | 1 + tests/components/soma/test_config_flow.py | 60 ++++++++++ 15 files changed, 374 insertions(+) create mode 100644 homeassistant/components/soma/.translations/en.json create mode 100644 homeassistant/components/soma/__init__.py create mode 100644 homeassistant/components/soma/config_flow.py create mode 100644 homeassistant/components/soma/const.py create mode 100644 homeassistant/components/soma/cover.py create mode 100644 homeassistant/components/soma/manifest.json create mode 100644 homeassistant/components/soma/strings.json create mode 100644 tests/components/soma/__init__.py create mode 100644 tests/components/soma/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index d42d7cbb3b3250..f28e9aaeda67e2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -599,6 +599,8 @@ omit = homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py homeassistant/components/solax/sensor.py + homeassistant/components/soma/cover.py + homeassistant/components/soma/__init__.py homeassistant/components/somfy/* homeassistant/components/somfy_mylink/* homeassistant/components/sonarr/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 4a6dfdbf6e62ab..db0ff3226c38ba 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -254,6 +254,7 @@ homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/smtp/* @fabaff homeassistant/components/solaredge_local/* @drobtravels @scheric homeassistant/components/solax/* @squishykid +homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne homeassistant/components/songpal/* @rytilahti homeassistant/components/spaceapi/* @fabaff diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json new file mode 100644 index 00000000000000..738d0fd642268f --- /dev/null +++ b/homeassistant/components/soma/.translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure one Soma Connect.", + "missing_configuration": "The Soma component is not configured. Please follow the documentation.", + "connection_error": "Connection to the specified device failed." + }, + "create_entry": { + "default": "Successfully authenticated with Soma." + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "title": "Set up Soma Connect" + } + }, + "title": "Soma" + } +} diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py new file mode 100644 index 00000000000000..5bf51e743e9c1a --- /dev/null +++ b/homeassistant/components/soma/__init__.py @@ -0,0 +1,111 @@ +"""Support for Soma Smartshades.""" +import logging + +import voluptuous as vol +from api.soma_api import SomaApi + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import DOMAIN, HOST, PORT, API + + +DEVICES = "devices" + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.string} + ) + }, + extra=vol.ALLOW_EXTRA, +) + +SOMA_COMPONENTS = ["cover"] + + +async def async_setup(hass, config): + """Set up the Soma component.""" + if DOMAIN not in config: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + data=config[DOMAIN], + context={"source": config_entries.SOURCE_IMPORT}, + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up Soma from a config entry.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][API] = SomaApi(entry.data[HOST], entry.data[PORT]) + devices = await hass.async_add_executor_job(hass.data[DOMAIN][API].list_devices) + hass.data[DOMAIN][DEVICES] = devices["shades"] + + for component in SOMA_COMPONENTS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload a config entry.""" + return True + + +class SomaEntity(Entity): + """Representation of a generic Soma device.""" + + def __init__(self, device, api): + """Initialize the Soma device.""" + self.device = device + self.api = api + self.current_position = 50 + + @property + def unique_id(self): + """Return the unique id base on the id returned by pysoma API.""" + return self.device["mac"] + + @property + def name(self): + """Return the name of the device.""" + return self.device["name"] + + @property + def device_info(self): + """Return device specific attributes. + + Implemented by platform classes. + """ + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Wazombi Labs", + } + + async def async_update(self): + """Update the device with the latest data.""" + response = await self.hass.async_add_executor_job( + self.api.get_shade_state, self.device["mac"] + ) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + return + self.current_position = 100 - response["position"] diff --git a/homeassistant/components/soma/config_flow.py b/homeassistant/components/soma/config_flow.py new file mode 100644 index 00000000000000..e2f89273520da4 --- /dev/null +++ b/homeassistant/components/soma/config_flow.py @@ -0,0 +1,56 @@ +"""Config flow for Soma.""" +import logging + +import voluptuous as vol +from api.soma_api import SomaApi +from requests import RequestException + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_PORT = 3000 + + +class SomaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Instantiate config flow.""" + + async def async_step_user(self, user_input=None): + """Handle a flow start.""" + if user_input is None: + data = { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + + return self.async_show_form(step_id="user", data_schema=vol.Schema(data)) + + return await self.async_step_creation(user_input) + + async def async_step_creation(self, user_input=None): + """Finish config flow.""" + api = SomaApi(user_input["host"], user_input["port"]) + try: + await self.hass.async_add_executor_job(api.list_devices) + _LOGGER.info("Successfully set up Soma Connect") + return self.async_create_entry( + title="Soma Connect", + data={"host": user_input["host"], "port": user_input["port"]}, + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed") + return self.async_abort(reason="connection_error") + + async def async_step_import(self, user_input=None): + """Handle flow start from existing config section.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="already_setup") + return await self.async_step_creation(user_input) diff --git a/homeassistant/components/soma/const.py b/homeassistant/components/soma/const.py new file mode 100644 index 00000000000000..815a0176e7e6fe --- /dev/null +++ b/homeassistant/components/soma/const.py @@ -0,0 +1,6 @@ +"""Define constants for the Soma component.""" + +DOMAIN = "soma" +HOST = "host" +PORT = "port" +API = "api" diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py new file mode 100644 index 00000000000000..1577b7f2911f03 --- /dev/null +++ b/homeassistant/components/soma/cover.py @@ -0,0 +1,79 @@ +"""Support for Soma Covers.""" + +import logging + +from homeassistant.components.cover import CoverDevice, ATTR_POSITION +from homeassistant.components.soma import DOMAIN, SomaEntity, DEVICES, API + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Soma cover platform.""" + + devices = hass.data[DOMAIN][DEVICES] + + async_add_entities( + [SomaCover(cover, hass.data[DOMAIN][API]) for cover in devices], True + ) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up platform. + + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + pass + + +class SomaCover(SomaEntity, CoverDevice): + """Representation of a Soma cover device.""" + + def close_cover(self, **kwargs): + """Close the cover.""" + response = self.api.set_shade_position(self.device["mac"], 100) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + + def open_cover(self, **kwargs): + """Open the cover.""" + response = self.api.set_shade_position(self.device["mac"], 0) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + + def stop_cover(self, **kwargs): + """Stop the cover.""" + # Set cover position to some value where up/down are both enabled + self.current_position = 50 + response = self.api.stop_shade(self.device["mac"]) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + + def set_cover_position(self, **kwargs): + """Move the cover shutter to a specific position.""" + self.current_position = kwargs[ATTR_POSITION] + response = self.api.set_shade_position( + self.device["mac"], 100 - kwargs[ATTR_POSITION] + ) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + + @property + def current_cover_position(self): + """Return the current position of cover shutter.""" + return self.current_position + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self.current_position == 0 diff --git a/homeassistant/components/soma/manifest.json b/homeassistant/components/soma/manifest.json new file mode 100644 index 00000000000000..35a77c063b89e6 --- /dev/null +++ b/homeassistant/components/soma/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "soma", + "name": "Soma Open API", + "config_flow": true, + "documentation": "", + "dependencies": [], + "codeowners": [ + "@ratsept" + ], + "requirements": [ + "pysoma==0.0.10" + ] +} diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json new file mode 100644 index 00000000000000..eac817ce1199dc --- /dev/null +++ b/homeassistant/components/soma/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure one Soma account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Soma component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Soma." + }, + "title": "Soma" + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ab7b339e58284a..21f57934e95bd6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -55,6 +55,7 @@ "smartthings", "smhi", "solaredge", + "soma", "somfy", "sonos", "tellduslive", diff --git a/requirements_all.txt b/requirements_all.txt index c25ca6a54fe400..5482af01cc2b09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1443,6 +1443,9 @@ pysmarty==0.8 # homeassistant.components.snmp pysnmp==4.4.11 +# homeassistant.components.soma +pysoma==0.0.10 + # homeassistant.components.sonos pysonos==0.0.23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2701513a6de7b9..801c09f322d910 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,6 +349,9 @@ pysmartapp==0.3.2 # homeassistant.components.smartthings pysmartthings==0.6.9 +# homeassistant.components.soma +pysoma==0.0.10 + # homeassistant.components.sonos pysonos==0.0.23 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 1e484e0dfc4853..9991a6bc1f0b60 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -143,6 +143,7 @@ "pysma", "pysmartapp", "pysmartthings", + "pysoma", "pysonos", "pyspcwebgw", "python_awair", diff --git a/tests/components/soma/__init__.py b/tests/components/soma/__init__.py new file mode 100644 index 00000000000000..8d84668e5ea9b9 --- /dev/null +++ b/tests/components/soma/__init__.py @@ -0,0 +1 @@ +"""Tests for the Soma component.""" diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py new file mode 100644 index 00000000000000..764a18d1b8b98a --- /dev/null +++ b/tests/components/soma/test_config_flow.py @@ -0,0 +1,60 @@ +"""Tests for the Soma config flow.""" +from unittest.mock import patch + +from api.soma_api import SomaApi +from requests import RequestException + +from homeassistant import data_entry_flow +from homeassistant.components.soma import config_flow, DOMAIN +from tests.common import MockConfigEntry + + +MOCK_HOST = "123.45.67.89" +MOCK_PORT = 3000 + + +async def test_form(hass): + """Test user form showing.""" + flow = config_flow.SomaFlowHandler() + flow.hass = hass + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_import_abort(hass): + """Test configuration from YAML aborting with existing entity.""" + flow = config_flow.SomaFlowHandler() + flow.hass = hass + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + result = await flow.async_step_import() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + +async def test_import_create(hass): + """Test configuration from YAML.""" + flow = config_flow.SomaFlowHandler() + flow.hass = hass + with patch.object(SomaApi, "list_devices", return_value={}): + result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_exception(hass): + """Test if RequestException fires when no connection can be made.""" + flow = config_flow.SomaFlowHandler() + flow.hass = hass + with patch.object(SomaApi, "list_devices", side_effect=RequestException()): + result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "connection_error" + + +async def test_full_flow(hass): + """Check classic use case.""" + hass.data[DOMAIN] = {} + flow = config_flow.SomaFlowHandler() + flow.hass = hass + with patch.object(SomaApi, "list_devices", return_value={}): + result = await flow.async_step_user({"host": MOCK_HOST, "port": MOCK_PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY From d116d2c1a47335941d1dce626a3f58e2550523be Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 30 Sep 2019 14:49:08 +0200 Subject: [PATCH 223/296] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 51c5cdb936d2f9..60eff866676462 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -245,24 +245,33 @@ stages: - template: templates/azp-step-ha-version.yaml@azure - script: | set -e + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + curl -o google-cloud-sdk.tar.gz https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz tar -C . -xvf google-cloud-sdk.tar.gz rm -f google-cloud-sdk.tar.gz ./google-cloud-sdk/install.sh displayName: 'Setup gCloud' - condition: eq(variables['homeassistantReleaseStable'], 'true')) + condition: eq(variables['homeassistantReleaseStable'], 'true') - script: | set -e + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - echo "$(gcloudAuth)" > gcloud_auth.json + + echo "$(gcloudAnalytic)" > gcloud_auth.json ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud_auth.json rm -f gcloud_auth.json displayName: 'Auth gCloud' - condition: eq(variables['homeassistantReleaseStable'], 'true')) + condition: eq(variables['homeassistantReleaseStable'], 'true') - script: | set -e + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver --update-env-vars VERSION=$(homeassistantRelease) + + ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver \ + --project home-assistant-analytics \ + --update-env-vars VERSION=$(homeassistantRelease) \ + --source gs://analytics-src/function-source.zip displayName: 'Push details to updater' - condition: eq(variables['homeassistantReleaseStable'], 'true')) + condition: eq(variables['homeassistantReleaseStable'], 'true') From d28980b0974bebfe5284adfc82189b8bed91d26f Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Mon, 30 Sep 2019 12:56:58 -0400 Subject: [PATCH 224/296] Bump pyecobee to 0.1.4 (#27074) --- homeassistant/components/ecobee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 131c35d7f89137..148e355a3d9e9a 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/ecobee", "dependencies": [], - "requirements": ["python-ecobee-api==0.1.3"], + "requirements": ["python-ecobee-api==0.1.4"], "codeowners": ["@marthoc"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5482af01cc2b09..f6f78bab918299 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1489,7 +1489,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.1.3 +python-ecobee-api==0.1.4 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 801c09f322d910..faf775ac5b844a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -359,7 +359,7 @@ pysonos==0.0.23 pyspcwebgw==0.4.0 # homeassistant.components.ecobee -python-ecobee-api==0.1.3 +python-ecobee-api==0.1.4 # homeassistant.components.darksky python-forecastio==1.4.0 From 8c01ed8a1ff92f9b00018b870025de67307d18ed Mon Sep 17 00:00:00 2001 From: John Luetke Date: Mon, 30 Sep 2019 11:26:26 -0700 Subject: [PATCH 225/296] Fix SSL connections to Pi-hole (#27073) --- homeassistant/components/pi_hole/__init__.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 00bb0e2d673486..95351083b5abfe 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -72,16 +72,10 @@ async def async_setup(hass, config): LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) - session = async_get_clientsession(hass, True) + session = async_get_clientsession(hass, verify_tls) pi_hole = PiHoleData( Hole( - host, - hass.loop, - session, - location=location, - tls=use_tls, - verify_tls=verify_tls, - api_token=api_key, + host, hass.loop, session, location=location, tls=use_tls, api_token=api_key ), name, ) From 9615ba3d99490e199a8f691a1be02626c04c5e25 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 30 Sep 2019 23:46:59 +0200 Subject: [PATCH 226/296] Bump shodan to 1.19.0 (#27079) --- homeassistant/components/shodan/manifest.json | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index be7f0a524dcd0b..fa704a6550a491 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -3,10 +3,10 @@ "name": "Shodan", "documentation": "https://www.home-assistant.io/components/shodan", "requirements": [ - "shodan==1.17.0" + "shodan==1.19.0" ], "dependencies": [], "codeowners": [ "@fabaff" ] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index f6f78bab918299..54a9ec2d438f68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1739,7 +1739,7 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.17.0 +shodan==1.19.0 # homeassistant.components.simplepush simplepush==1.1.4 From 513d2652e4774589caa97db29c6ed68b01ca18f9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 1 Oct 2019 00:32:19 +0000 Subject: [PATCH 227/296] [ci skip] Translation update --- .../cert_expiry/.translations/pt-BR.json | 7 ++++-- .../deconz/.translations/pt-BR.json | 11 +++++++++ .../components/ecobee/.translations/lb.json | 12 ++++++++++ .../ecobee/.translations/pt-BR.json | 24 +++++++++++++++++++ .../geonetnz_quakes/.translations/pt-BR.json | 17 +++++++++++++ .../life360/.translations/pt-BR.json | 1 + .../components/light/.translations/pt-BR.json | 13 ++++++++++ .../components/linky/.translations/pt-BR.json | 19 +++++++++++++++ .../components/plex/.translations/pt-BR.json | 13 ++++++++++ .../components/soma/.translations/ca.json | 13 ++++++++++ .../components/soma/.translations/da.json | 12 ++++++++++ .../components/soma/.translations/en.json | 19 ++++----------- .../components/soma/.translations/it.json | 13 ++++++++++ .../components/soma/.translations/ru.json | 13 ++++++++++ .../components/soma/.translations/sl.json | 13 ++++++++++ .../traccar/.translations/pt-BR.json | 13 ++++++++++ .../transmission/.translations/pt-BR.json | 3 +++ .../twentemilieu/.translations/pt-BR.json | 23 ++++++++++++++++++ .../components/unifi/.translations/pt-BR.json | 2 ++ .../velbus/.translations/pt-BR.json | 11 +++++++++ .../components/zha/.translations/lb.json | 4 ++++ .../components/zha/.translations/no.json | 4 ++++ .../components/zha/.translations/pt-BR.json | 1 + 23 files changed, 244 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/ecobee/.translations/pt-BR.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/pt-BR.json create mode 100644 homeassistant/components/light/.translations/pt-BR.json create mode 100644 homeassistant/components/linky/.translations/pt-BR.json create mode 100644 homeassistant/components/plex/.translations/pt-BR.json create mode 100644 homeassistant/components/soma/.translations/ca.json create mode 100644 homeassistant/components/soma/.translations/da.json create mode 100644 homeassistant/components/soma/.translations/it.json create mode 100644 homeassistant/components/soma/.translations/ru.json create mode 100644 homeassistant/components/soma/.translations/sl.json create mode 100644 homeassistant/components/twentemilieu/.translations/pt-BR.json create mode 100644 homeassistant/components/velbus/.translations/pt-BR.json diff --git a/homeassistant/components/cert_expiry/.translations/pt-BR.json b/homeassistant/components/cert_expiry/.translations/pt-BR.json index d26f0f9470e7e0..06534314e0099a 100644 --- a/homeassistant/components/cert_expiry/.translations/pt-BR.json +++ b/homeassistant/components/cert_expiry/.translations/pt-BR.json @@ -6,6 +6,7 @@ "error": { "certificate_fetch_failed": "N\u00e3o \u00e9 poss\u00edvel buscar o certificado dessa combina\u00e7\u00e3o de host e porta", "connection_timeout": "Tempo limite ao conectar-se a este host", + "host_port_exists": "Essa combina\u00e7\u00e3o de host e porta j\u00e1 est\u00e1 configurada", "resolve_failed": "Este host n\u00e3o pode ser resolvido" }, "step": { @@ -14,8 +15,10 @@ "host": "O nome do host do certificado", "name": "O nome do certificado", "port": "A porta do certificado" - } + }, + "title": "Defina o certificado para testar" } - } + }, + "title": "Expira\u00e7\u00e3o do certificado" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pt-BR.json b/homeassistant/components/deconz/.translations/pt-BR.json index d066cbcc510deb..8d54c470846b21 100644 --- a/homeassistant/components/deconz/.translations/pt-BR.json +++ b/homeassistant/components/deconz/.translations/pt-BR.json @@ -40,5 +40,16 @@ } }, "title": "Gateway deCONZ Zigbee" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Permitir sensores deCONZ CLIP", + "allow_deconz_groups": "Permitir grupos de luz deCONZ" + }, + "description": "Configurar visibilidade dos tipos de dispositivos deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/lb.json b/homeassistant/components/ecobee/.translations/lb.json index 1982dd40840283..ee1fd5246c0788 100644 --- a/homeassistant/components/ecobee/.translations/lb.json +++ b/homeassistant/components/ecobee/.translations/lb.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "one_instance_only": "D\u00ebs Integratioun \u00ebnnerst\u00ebtzt n\u00ebmmen eng ecobee Instanz." + }, + "error": { + "pin_request_failed": "Feeler beim ufroe vum PIN vun ecobee; iwwerpr\u00e9ift op den API Schl\u00ebssel korrekt ass.", + "token_request_failed": "Feeler beim ufroe vum Jeton vun ecobee; prob\u00e9iert nach emol." + }, "step": { + "authorize": { + "description": "Autoris\u00e9iert d\u00ebs App op https://www.ecobee.com/consumerportal/index.html mam Pin Code:\n\n{pin}\n\nKlickt dann op ofsch\u00e9cken.", + "title": "App autoris\u00e9ieren op ecobee.com" + }, "user": { "data": { "api_key": "API Schl\u00ebssel" }, + "description": "Gitt den API Schl\u00ebssel vun ecobee.com an:", "title": "ecobee API Schl\u00ebssel" } }, diff --git a/homeassistant/components/ecobee/.translations/pt-BR.json b/homeassistant/components/ecobee/.translations/pt-BR.json new file mode 100644 index 00000000000000..65394faba177c2 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "one_instance_only": "Essa integra\u00e7\u00e3o atualmente suporta apenas uma inst\u00e2ncia ecobee." + }, + "error": { + "token_request_failed": "Erro ao solicitar tokens da ecobee; Por favor, tente novamente." + }, + "step": { + "authorize": { + "description": "Por favor, autorize este aplicativo em https://www.ecobee.com/consumerportal/index.html com c\u00f3digo PIN:\n\n{pin}\n\nEm seguida, pressione Submit.", + "title": "Autorizar aplicativo em ecobee.com" + }, + "user": { + "data": { + "api_key": "Chave API" + }, + "description": "Por favor, insira a chave de API obtida em ecobee.com.", + "title": "chave da API ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json b/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json new file mode 100644 index 00000000000000..7e3ee3b24dab4c --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Localiza\u00e7\u00e3o j\u00e1 registrada" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "Preencha os detalhes do filtro." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/pt-BR.json b/homeassistant/components/life360/.translations/pt-BR.json index ca4cee896b37ac..5181c37969a585 100644 --- a/homeassistant/components/life360/.translations/pt-BR.json +++ b/homeassistant/components/life360/.translations/pt-BR.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Credenciais inv\u00e1lidas", "invalid_username": "Nome de usu\u00e1rio Inv\u00e1lido", + "unexpected": "Erro inesperado na comunica\u00e7\u00e3o com o servidor Life360", "user_already_configured": "A conta j\u00e1 foi configurada" }, "step": { diff --git a/homeassistant/components/light/.translations/pt-BR.json b/homeassistant/components/light/.translations/pt-BR.json new file mode 100644 index 00000000000000..05414b1e03c55f --- /dev/null +++ b/homeassistant/components/light/.translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Alternar {entity_name}", + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/pt-BR.json b/homeassistant/components/linky/.translations/pt-BR.json new file mode 100644 index 00000000000000..23f519353b4852 --- /dev/null +++ b/homeassistant/components/linky/.translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "username_exists": "Conta j\u00e1 configurada", + "wrong_login": "Erro de Login: por favor, verifique seu e-mail e senha" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "E-mail" + }, + "description": "Insira suas credenciais", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pt-BR.json b/homeassistant/components/plex/.translations/pt-BR.json new file mode 100644 index 00000000000000..9a759e309c2a10 --- /dev/null +++ b/homeassistant/components/plex/.translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostrar todos os controles", + "use_episode_art": "Usar arte epis\u00f3dio" + }, + "description": "Op\u00e7\u00f5es para Plex Media Players" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ca.json b/homeassistant/components/soma/.translations/ca.json new file mode 100644 index 00000000000000..6bd4737d6fc802 --- /dev/null +++ b/homeassistant/components/soma/.translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s pots configurar un compte de Soma.", + "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "missing_configuration": "El component Soma no est\u00e0 configurat. Mira'n la documentaci\u00f3." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa amb Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/da.json b/homeassistant/components/soma/.translations/da.json new file mode 100644 index 00000000000000..460f01e301feba --- /dev/null +++ b/homeassistant/components/soma/.translations/da.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere en Soma-konto.", + "missing_configuration": "Soma-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen." + }, + "create_entry": { + "default": "Godkendt med Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json index 738d0fd642268f..5dea73fcc22a2e 100644 --- a/homeassistant/components/soma/.translations/en.json +++ b/homeassistant/components/soma/.translations/en.json @@ -1,24 +1,13 @@ { "config": { "abort": { - "already_setup": "You can only configure one Soma Connect.", - "missing_configuration": "The Soma component is not configured. Please follow the documentation.", - "connection_error": "Connection to the specified device failed." + "already_setup": "You can only configure one Soma account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Soma component is not configured. Please follow the documentation." }, "create_entry": { "default": "Successfully authenticated with Soma." }, - "step": { - "user": { - "data": { - "host": "Host", - "password": "Password", - "port": "Port", - "username": "Username" - }, - "title": "Set up Soma Connect" - } - }, "title": "Soma" } -} +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/it.json b/homeassistant/components/soma/.translations/it.json new file mode 100644 index 00000000000000..ce8e950daccc5c --- /dev/null +++ b/homeassistant/components/soma/.translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare un solo account Soma.", + "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", + "missing_configuration": "Il componente Soma non \u00e8 configurato. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticato con successo con Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ru.json b/homeassistant/components/soma/.translations/ru.json new file mode 100644 index 00000000000000..5ab3af0ecf8895 --- /dev/null +++ b/homeassistant/components/soma/.translations/ru.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/sl.json b/homeassistant/components/soma/.translations/sl.json new file mode 100644 index 00000000000000..7dd523f366c443 --- /dev/null +++ b/homeassistant/components/soma/.translations/sl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Nastavite lahko samo en ra\u010dun Soma.", + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "missing_configuration": "Komponenta Soma ni konfigurirana. Upo\u0161tevajte dokumentacijo." + }, + "create_entry": { + "default": "Uspe\u0161no overjen s Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/pt-BR.json b/homeassistant/components/traccar/.translations/pt-BR.json index 9fc23b3e394074..4fa0c4e6714f73 100644 --- a/homeassistant/components/traccar/.translations/pt-BR.json +++ b/homeassistant/components/traccar/.translations/pt-BR.json @@ -1,5 +1,18 @@ { "config": { + "abort": { + "not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel na Internet para receber mensagens do Traccar.", + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos ao Home Assistant, voc\u00ea precisar\u00e1 configurar o recurso de webhook no Traccar. \n\n Use o seguinte URL: ` {webhook_url} ` \n\n Veja [a documenta\u00e7\u00e3o] ({docs_url}) para mais detalhes." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o Traccar?", + "title": "Configurar Traccar" + } + }, "title": "Traccar" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pt-BR.json b/homeassistant/components/transmission/.translations/pt-BR.json index a2d3d177e22d9f..cabbb6d91494c1 100644 --- a/homeassistant/components/transmission/.translations/pt-BR.json +++ b/homeassistant/components/transmission/.translations/pt-BR.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + }, "error": { "cannot_connect": "N\u00e3o foi poss\u00edvel conectar ao host", "wrong_credentials": "Nome de usu\u00e1rio ou senha incorretos" diff --git a/homeassistant/components/twentemilieu/.translations/pt-BR.json b/homeassistant/components/twentemilieu/.translations/pt-BR.json new file mode 100644 index 00000000000000..73735dda1d9752 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Endere\u00e7o j\u00e1 configurado." + }, + "error": { + "connection_error": "Falha ao conectar.", + "invalid_address": "Endere\u00e7o n\u00e3o encontrado na \u00e1rea de servi\u00e7o de Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Carta da casa/adicional", + "house_number": "N\u00famero da casa", + "post_code": "C\u00f3digo postal" + }, + "description": "Configure o Twente Milieu, fornecendo informa\u00e7\u00f5es de coleta de lixo em seu endere\u00e7o.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/pt-BR.json b/homeassistant/components/unifi/.translations/pt-BR.json index ea13035e09bec1..113eaa000fe620 100644 --- a/homeassistant/components/unifi/.translations/pt-BR.json +++ b/homeassistant/components/unifi/.translations/pt-BR.json @@ -27,7 +27,9 @@ "step": { "device_tracker": { "data": { + "detection_time": "Tempo em segundos desde a \u00faltima vez que foi visto at\u00e9 ser considerado afastado", "track_clients": "Rastrear clientes da rede", + "track_devices": "Rastrear dispositivos de rede (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de rede com fio" } } diff --git a/homeassistant/components/velbus/.translations/pt-BR.json b/homeassistant/components/velbus/.translations/pt-BR.json new file mode 100644 index 00000000000000..cb2031dc7e556e --- /dev/null +++ b/homeassistant/components/velbus/.translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "port_exists": "Esta porta j\u00e1 est\u00e1 configurada" + }, + "error": { + "connection_failed": "A conex\u00e3o velbus falhou", + "port_exists": "Esta porta j\u00e1 est\u00e1 configurada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/lb.json b/homeassistant/components/zha/.translations/lb.json index 49a754f1da587f..a289e05e6676bc 100644 --- a/homeassistant/components/zha/.translations/lb.json +++ b/homeassistant/components/zha/.translations/lb.json @@ -18,6 +18,10 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Mellen", + "warn": "Warnen" + }, "trigger_subtype": { "both_buttons": "B\u00e9id Kn\u00e4ppchen", "button_1": "\u00c9ischte Kn\u00e4ppchen", diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 623c33637e0e9d..95550ca0999966 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -18,6 +18,10 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Advarer" + }, "trigger_subtype": { "both_buttons": "Begge knapper", "button_1": "F\u00f8rste knapp", diff --git a/homeassistant/components/zha/.translations/pt-BR.json b/homeassistant/components/zha/.translations/pt-BR.json index 0bc3afe28eca5d..7ccc661dd28d56 100644 --- a/homeassistant/components/zha/.translations/pt-BR.json +++ b/homeassistant/components/zha/.translations/pt-BR.json @@ -19,6 +19,7 @@ }, "device_automation": { "action_type": { + "squawk": "Squawk", "warn": "Aviso" } } From bce49233ca64d9ec191259bae5855860429f734f Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Mon, 30 Sep 2019 17:42:06 -0700 Subject: [PATCH 228/296] Add some icons for Obihai (#27075) * Add some icons for Obihai * Lint * Lint * Lint fixes --- homeassistant/components/obihai/sensor.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 4644875ee8b164..89bfee7d4eebfe 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -111,6 +111,25 @@ def device_class(self): return DEVICE_CLASS_TIMESTAMP return None + @property + def icon(self): + """Return an icon.""" + if self._service_name == "Call Direction": + if self._state == "No Active Calls": + return "mdi:phone-off" + if self._state == "Inbound Call": + return "mdi:phone-incoming" + return "mdi:phone-outgoing" + if "Caller Info" in self._service_name: + return "mdi:phone-log" + if "Port" in self._service_name: + if self._state == "Ringing": + return "mdi:phone-ring" + if self._state == "Off Hook": + return "mdi:phone-in-talk" + return "mdi:phone-hangup" + return "mdi:phone" + def update(self): """Update the sensor.""" services = self._pyobihai.get_state() From a9398a362f61e4501e93d33a7ff6bee1b48388ce Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 1 Oct 2019 10:46:33 +1000 Subject: [PATCH 229/296] bumped version of upstream library (#27083) --- homeassistant/components/geonetnz_quakes/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index c84a4152582a60..77f3c64752e9f5 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/geonetnz_quakes", "requirements": [ - "aio_geojson_geonetnz_quakes==0.9" + "aio_geojson_geonetnz_quakes==0.10" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 54a9ec2d438f68..561e3345417251 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -121,7 +121,7 @@ adguardhome==0.2.1 afsapi==0.0.4 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.9 +aio_geojson_geonetnz_quakes==0.10 # homeassistant.components.ambient_station aioambient==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index faf775ac5b844a..e7e4ed37e0012f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -43,7 +43,7 @@ YesssSMS==0.4.1 adguardhome==0.2.1 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.9 +aio_geojson_geonetnz_quakes==0.10 # homeassistant.components.ambient_station aioambient==0.3.2 From e2d7a01d65104efe47fcc58f05151545dc82d60e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 1 Oct 2019 06:19:51 +0200 Subject: [PATCH 230/296] Remove last of device tracker scanner (#27082) --- homeassistant/components/unifi/device_tracker.py | 12 ++---------- tests/components/unifi/test_device_tracker.py | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b3982e7327d42d..ad04b8a0eb37da 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,9 +1,8 @@ """Track devices using UniFi controllers.""" import logging -import voluptuous as vol from homeassistant.components.unifi.config_flow import get_controller_from_config_entry -from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER from homeassistant.core import callback @@ -39,13 +38,6 @@ "vlan", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) - - -async def async_setup_scanner(hass, config, sync_see, discovery_info): - """Set up the Unifi integration.""" - return True - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up device tracker for UniFi component.""" @@ -59,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if ( entity.config_entry_id == config_entry.entry_id - and entity.domain == DOMAIN + and entity.domain == DEVICE_TRACKER_DOMAIN and "-" in entity.unique_id ): diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 969c2a734d3448..760e1e4fa4c50c 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -163,12 +163,12 @@ async def setup_controller(hass, mock_controller, options={}): async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a bridge.""" + """Test that nothing happens when configuring unifi through device tracker platform.""" assert ( await async_setup_component( hass, device_tracker.DOMAIN, {device_tracker.DOMAIN: {"platform": "unifi"}} ) - is True + is False ) assert unifi.DOMAIN not in hass.data From a1997ee891be5ed5b5c2d85b6eb738fdf9bc0e00 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 1 Oct 2019 05:35:10 +0100 Subject: [PATCH 231/296] Bugfix evohome (#26810) * address issues #25984, #25985 * small tweak * refactor - fix bugs, coding erros, consolidate * some zones don't have schedules * some zones don't have schedules 2 * some zones don't have schedules 3 * fix water_heater, add away mode * readbility tweak * bugfix: no refesh after state change * bugfix: no refesh after state change 2 * temove dodgy wrappers (protected-access), fix until logic * remove dodgy _set_zone_mode wrapper * tweak * tweak docstrings * refactor as per PR review * refactor as per PR review 3 * refactor to use dt_util * small tweak * tweak doc strings * remove packet from _refresh * set_temp() don't have until * add unique_id * add unique_id 2 --- homeassistant/components/evohome/__init__.py | 247 +++++++++++------- homeassistant/components/evohome/climate.py | 240 ++++++++--------- homeassistant/components/evohome/const.py | 2 - .../components/evohome/water_heater.py | 92 ++++--- 4 files changed, 315 insertions(+), 266 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index ba7a72024ed16b..14bf12239538b2 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -4,6 +4,7 @@ """ from datetime import datetime, timedelta import logging +import re from typing import Any, Dict, Optional, Tuple import aiohttp.client_exceptions @@ -25,9 +26,9 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.util.dt import parse_datetime, utcnow +import homeassistant.util.dt as dt_util -from .const import DOMAIN, STORAGE_VERSION, STORAGE_KEY, GWS, TCS +from .const import DOMAIN, EVO_FOLLOW, STORAGE_VERSION, STORAGE_KEY, GWS, TCS _LOGGER = logging.getLogger(__name__) @@ -55,20 +56,45 @@ ) -def _local_dt_to_utc(dt_naive: datetime) -> datetime: - dt_aware = utcnow() + (dt_naive - datetime.now()) +def _local_dt_to_aware(dt_naive: datetime) -> datetime: + dt_aware = dt_util.now() + (dt_naive - datetime.now()) if dt_aware.microsecond >= 500000: dt_aware += timedelta(seconds=1) return dt_aware.replace(microsecond=0) -def _utc_to_local_dt(dt_aware: datetime) -> datetime: - dt_naive = datetime.now() + (dt_aware - utcnow()) +def _dt_to_local_naive(dt_aware: datetime) -> datetime: + dt_naive = datetime.now() + (dt_aware - dt_util.now()) if dt_naive.microsecond >= 500000: dt_naive += timedelta(seconds=1) return dt_naive.replace(microsecond=0) +def convert_until(status_dict, until_key) -> str: + """Convert datetime string from "%Y-%m-%dT%H:%M:%SZ" to local/aware/isoformat.""" + if until_key in status_dict: # only present for certain modes + dt_utc_naive = dt_util.parse_datetime(status_dict[until_key]) + status_dict[until_key] = dt_util.as_local(dt_utc_naive).isoformat() + + +def convert_dict(dictionary: Dict[str, Any]) -> Dict[str, Any]: + """Recursively convert a dict's keys to snake_case.""" + + def convert_key(key: str) -> str: + """Convert a string to snake_case.""" + string = re.sub(r"[\-\.\s]", "_", str(key)) + return (string[0]).lower() + re.sub( + r"[A-Z]", lambda matched: "_" + matched.group(0).lower(), string[1:] + ) + + return { + (convert_key(k) if isinstance(k, str) else k): ( + convert_dict(v) if isinstance(v, dict) else v + ) + for k, v in dictionary.items() + } + + def _handle_exception(err) -> bool: try: raise err @@ -135,7 +161,7 @@ class EvoBroker: """Container for evohome client and data.""" def __init__(self, hass, params) -> None: - """Initialize the evohome client and data structure.""" + """Initialize the evohome client and its data structure.""" self.hass = hass self.params = params self.config = {} @@ -157,7 +183,7 @@ async def init_client(self) -> bool: # evohomeasync2 uses naive/local datetimes if access_token_expires is not None: - access_token_expires = _utc_to_local_dt(access_token_expires) + access_token_expires = _dt_to_local_naive(access_token_expires) client = self.client = evohomeasync2.EvohomeClient( self.params[CONF_USERNAME], @@ -220,7 +246,7 @@ async def _load_auth_tokens( access_token = app_storage.get(CONF_ACCESS_TOKEN) at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES) if at_expires_str: - at_expires_dt = parse_datetime(at_expires_str) + at_expires_dt = dt_util.parse_datetime(at_expires_str) else: at_expires_dt = None @@ -230,7 +256,7 @@ async def _load_auth_tokens( async def _save_auth_tokens(self, *args) -> None: # evohomeasync2 uses naive/local datetimes - access_token_expires = _local_dt_to_utc(self.client.access_token_expires) + access_token_expires = _local_dt_to_aware(self.client.access_token_expires) self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME] self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token @@ -246,11 +272,11 @@ async def _save_auth_tokens(self, *args) -> None: ) async def update(self, *args, **kwargs) -> None: - """Get the latest state data of the entire evohome Location. + """Get the latest state data of an entire evohome Location. - This includes state data for the Controller and all its child devices, - such as the operating mode of the Controller and the current temp of - its children (e.g. Zones, DHW controller). + This includes state data for a Controller and all its child devices, such as the + operating mode of the Controller and the current temp of its children (e.g. + Zones, DHW controller). """ loc_idx = self.params[CONF_LOCATION_IDX] @@ -260,9 +286,7 @@ async def update(self, *args, **kwargs) -> None: _handle_exception(err) else: # inform the evohome devices that state data has been updated - self.hass.helpers.dispatcher.async_dispatcher_send( - DOMAIN, {"signal": "refresh"} - ) + self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) @@ -270,8 +294,8 @@ async def update(self, *args, **kwargs) -> None: class EvoDevice(Entity): """Base for any evohome device. - This includes the Controller, (up to 12) Heating Zones and - (optionally) a DHW controller. + This includes the Controller, (up to 12) Heating Zones and (optionally) a + DHW controller. """ def __init__(self, evo_broker, evo_device) -> None: @@ -280,72 +304,26 @@ def __init__(self, evo_broker, evo_device) -> None: self._evo_broker = evo_broker self._evo_tcs = evo_broker.tcs - self._name = self._icon = self._precision = None - self._state_attributes = [] + self._unique_id = self._name = self._icon = self._precision = None + self._device_state_attrs = {} + self._state_attributes = [] self._supported_features = None - self._schedule = {} @callback - def _refresh(self, packet): - if packet["signal"] == "refresh": - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def setpoints(self) -> Dict[str, Any]: - """Return the current/next setpoints from the schedule. - - Only Zones & DHW controllers (but not the TCS) can have schedules. - """ - if not self._schedule["DailySchedules"]: - return {} - - switchpoints = {} - - day_time = datetime.now() - day_of_week = int(day_time.strftime("%w")) # 0 is Sunday - - # Iterate today's switchpoints until past the current time of day... - day = self._schedule["DailySchedules"][day_of_week] - sp_idx = -1 # last switchpoint of the day before - for i, tmp in enumerate(day["Switchpoints"]): - if day_time.strftime("%H:%M:%S") > tmp["TimeOfDay"]: - sp_idx = i # current setpoint - else: - break - - # Did the current SP start yesterday? Does the next start SP tomorrow? - current_sp_day = -1 if sp_idx == -1 else 0 - next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 - - for key, offset, idx in [ - ("current", current_sp_day, sp_idx), - ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), - ]: - - spt = switchpoints[key] = {} - - sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") - day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] - switchpoint = day["Switchpoints"][idx] - - dt_naive = datetime.strptime( - f"{sp_date}T{switchpoint['TimeOfDay']}", "%Y-%m-%dT%H:%M:%S" - ) - - spt["from"] = _local_dt_to_utc(dt_naive).isoformat() - try: - spt["temperature"] = switchpoint["heatSetpoint"] - except KeyError: - spt["state"] = switchpoint["DhwState"] - - return switchpoints + def _refresh(self) -> None: + self.async_schedule_update_ha_state(force_refresh=True) @property def should_poll(self) -> bool: """Evohome entities should not be polled.""" return False + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + @property def name(self) -> str: """Return the name of the Evohome entity.""" @@ -354,15 +332,15 @@ def name(self) -> str: @property def device_state_attributes(self) -> Dict[str, Any]: """Return the Evohome-specific state attributes.""" - status = {} - for attr in self._state_attributes: - if attr != "setpoints": - status[attr] = getattr(self._evo_device, attr) - - if "setpoints" in self._state_attributes: - status["setpoints"] = self.setpoints + status = self._device_state_attrs + if "systemModeStatus" in status: + convert_until(status["systemModeStatus"], "timeUntil") + if "setpointStatus" in status: + convert_until(status["setpointStatus"], "until") + if "stateStatus" in status: + convert_until(status["stateStatus"], "until") - return {"status": status} + return {"status": convert_dict(status)} @property def icon(self) -> str: @@ -388,27 +366,98 @@ def temperature_unit(self) -> str: """Return the temperature unit to use in the frontend UI.""" return TEMP_CELSIUS - async def _call_client_api(self, api_function) -> None: + async def _call_client_api(self, api_function, refresh=True) -> Any: try: - await api_function + result = await api_function except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - _handle_exception(err) + if not _handle_exception(err): + return - self.hass.helpers.event.async_call_later( - 2, self._evo_broker.update() - ) # call update() in 2 seconds + if refresh is True: + self.hass.helpers.event.async_call_later(1, self._evo_broker.update()) - async def _update_schedule(self) -> None: - """Get the latest state data.""" - if ( - not self._schedule.get("DailySchedules") - or parse_datetime(self.setpoints["next"]["from"]) < utcnow() - ): + return result + + +class EvoChild(EvoDevice): + """Base for any evohome child. + + This includes (up to 12) Heating Zones and (optionally) a DHW controller. + """ + + def __init__(self, evo_broker, evo_device) -> None: + """Initialize a evohome Controller (hub).""" + super().__init__(evo_broker, evo_device) + self._schedule = {} + self._setpoints = {} + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature of a Zone.""" + if self._evo_device.temperatureStatus["isAvailable"]: + return self._evo_device.temperatureStatus["temperature"] + return None + + @property + def setpoints(self) -> Dict[str, Any]: + """Return the current/next setpoints from the schedule. + + Only Zones & DHW controllers (but not the TCS) can have schedules. + """ + if not self._schedule["DailySchedules"]: + return {} # no schedule {'DailySchedules': []}, so no scheduled setpoints + + day_time = dt_util.now() + day_of_week = int(day_time.strftime("%w")) # 0 is Sunday + time_of_day = day_time.strftime("%H:%M:%S") + + # Iterate today's switchpoints until past the current time of day... + day = self._schedule["DailySchedules"][day_of_week] + sp_idx = -1 # last switchpoint of the day before + for i, tmp in enumerate(day["Switchpoints"]): + if time_of_day > tmp["TimeOfDay"]: + sp_idx = i # current setpoint + else: + break + + # Did the current SP start yesterday? Does the next start SP tomorrow? + this_sp_day = -1 if sp_idx == -1 else 0 + next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 + + for key, offset, idx in [ + ("this", this_sp_day, sp_idx), + ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), + ]: + sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") + day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] + switchpoint = day["Switchpoints"][idx] + + dt_local_aware = _local_dt_to_aware( + dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}") + ) + + self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat() try: - self._schedule = await self._evo_device.schedule() - except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - _handle_exception(err) + self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"] + except KeyError: + self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"] + + return self._setpoints + + async def _update_schedule(self) -> None: + """Get the latest schedule.""" + if "DailySchedules" in self._schedule and not self._schedule["DailySchedules"]: + if not self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW: + return # avoid unnecessary I/O - there's nothing to update + + self._schedule = await self._call_client_api( + self._evo_device.schedule(), refresh=False + ) async def async_update(self) -> None: """Get the latest state data.""" - await self._update_schedule() + next_sp_from = self._setpoints.get("next_sp_from", "2000-01-01T00:00:00+00:00") + if dt_util.now() >= dt_util.parse_datetime(next_sp_from): + await self._update_schedule() # no schedule, or it's out-of-date + + self._device_state_attrs = {"setpoints": self.setpoints} diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 0264f76f38f5db..e5c8c6af14bde1 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -1,7 +1,6 @@ """Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems.""" -from datetime import datetime import logging -from typing import Any, Dict, Optional, List +from typing import Optional, List from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -22,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime -from . import CONF_LOCATION_IDX, EvoDevice +from . import CONF_LOCATION_IDX, EvoDevice, EvoChild from .const import ( DOMAIN, EVO_RESET, @@ -61,6 +60,9 @@ } HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items()} +STATE_ATTRS_TCS = ["systemId", "activeFaults", "systemModeStatus"] +STATE_ATTRS_ZONES = ["zoneId", "activeFaults", "setpointStatus", "temperatureStatus"] + async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None @@ -114,63 +116,20 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): """Base for a Honeywell evohome Climate device.""" def __init__(self, evo_broker, evo_device) -> None: - """Initialize the evohome Climate device.""" + """Initialize a Climate device.""" super().__init__(evo_broker, evo_device) self._preset_modes = None - async def _set_temperature( - self, temperature: float, until: Optional[datetime] = None - ) -> None: - """Set a new target temperature for the Zone. - - until == None means indefinitely (i.e. PermanentOverride) - """ - await self._call_client_api( - self._evo_device.set_temperature(temperature, until) - ) - - async def _set_zone_mode(self, op_mode: str) -> None: - """Set a Zone to one of its native EVO_* operating modes. - - Zones inherit their _effective_ operating mode from the Controller. - - Usually, Zones are in 'FollowSchedule' mode, where their setpoints are - a function of their own schedule and the Controller's operating mode, - e.g. 'AutoWithEco' mode means their setpoint is (by default) 3C less - than scheduled. - - However, Zones can _override_ these setpoints, either indefinitely, - 'PermanentOverride' mode, or for a period of time, 'TemporaryOverride', - after which they will revert back to 'FollowSchedule'. - - Finally, some of the Controller's operating modes are _forced_ upon the - Zones, regardless of any override mode, e.g. 'HeatingOff', Zones to - (by default) 5C, and 'Away', Zones to (by default) 12C. - """ - if op_mode == EVO_FOLLOW: - await self._call_client_api(self._evo_device.cancel_temp_override()) - return - - temperature = self._evo_device.setpointStatus["targetHeatTemperature"] - until = None # EVO_PERMOVER - - if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]: - await self._update_schedule() - if self._schedule["DailySchedules"]: - until = parse_datetime(self.setpoints["next"]["from"]) - - await self._set_temperature(temperature, until=until) - async def _set_tcs_mode(self, op_mode: str) -> None: - """Set the Controller to any of its native EVO_* operating modes.""" + """Set a Controller to any of its native EVO_* operating modes.""" await self._call_client_api( self._evo_tcs._set_status(op_mode) # pylint: disable=protected-access ) @property def hvac_modes(self) -> List[str]: - """Return the list of available hvac operation modes.""" + """Return a list of available hvac operation modes.""" return list(HA_HVAC_TO_TCS) @property @@ -179,36 +138,24 @@ def preset_modes(self) -> Optional[List[str]]: return self._preset_modes -class EvoZone(EvoClimateDevice): +class EvoZone(EvoChild, EvoClimateDevice): """Base for a Honeywell evohome Zone.""" def __init__(self, evo_broker, evo_device) -> None: - """Initialize the evohome Zone.""" + """Initialize a Zone.""" super().__init__(evo_broker, evo_device) + self._unique_id = evo_device.zoneId self._name = evo_device.name self._icon = "mdi:radiator" self._precision = self._evo_device.setpointCapabilities["valueResolution"] - self._state_attributes = [ - "zoneId", - "activeFaults", - "setpointStatus", - "temperatureStatus", - "setpoints", - ] - self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE self._preset_modes = list(HA_PRESET_TO_EVO) - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._evo_device.temperatureStatus["isAvailable"] - @property def hvac_mode(self) -> str: - """Return the current operating mode of the evohome Zone.""" + """Return the current operating mode of a Zone.""" if self._evo_tcs.systemModeStatus["mode"] in [EVO_AWAY, EVO_HEATOFF]: return HVAC_MODE_AUTO is_off = self.target_temperature <= self.min_temp @@ -221,24 +168,15 @@ def hvac_action(self) -> Optional[str]: return CURRENT_HVAC_OFF if self.target_temperature <= self.min_temp: return CURRENT_HVAC_OFF - if self.target_temperature < self.current_temperature: + if not self._evo_device.temperatureStatus["isAvailable"]: + return None + if self.target_temperature <= self.current_temperature: return CURRENT_HVAC_IDLE return CURRENT_HVAC_HEAT - @property - def current_temperature(self) -> Optional[float]: - """Return the current temperature of the evohome Zone.""" - return ( - self._evo_device.temperatureStatus["temperature"] - if self._evo_device.temperatureStatus["isAvailable"] - else None - ) - @property def target_temperature(self) -> float: - """Return the target temperature of the evohome Zone.""" - if self._evo_tcs.systemModeStatus["mode"] == EVO_HEATOFF: - return self._evo_device.setpointCapabilities["minHeatSetpoint"] + """Return the target temperature of a Zone.""" return self._evo_device.setpointStatus["targetHeatTemperature"] @property @@ -252,7 +190,7 @@ def preset_mode(self) -> Optional[str]: @property def min_temp(self) -> float: - """Return the minimum target temperature of a evohome Zone. + """Return the minimum target temperature of a Zone. The default is 5, but is user-configurable within 5-35 (in Celsius). """ @@ -260,7 +198,7 @@ def min_temp(self) -> float: @property def max_temp(self) -> float: - """Return the maximum target temperature of a evohome Zone. + """Return the maximum target temperature of a Zone. The default is 35, but is user-configurable within 5-35 (in Celsius). """ @@ -268,26 +206,70 @@ def max_temp(self) -> float: async def async_set_temperature(self, **kwargs) -> None: """Set a new target temperature.""" - until = kwargs.get("until") - if until: - until = parse_datetime(until) + temperature = kwargs["temperature"] - await self._set_temperature(kwargs["temperature"], until) + if self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW: + await self._update_schedule() + until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + elif self._evo_device.setpointStatus["setpointMode"] == EVO_TEMPOVER: + until = parse_datetime(self._evo_device.setpointStatus["until"]) + else: # EVO_PERMOVER + until = None + + await self._call_client_api( + self._evo_device.set_temperature(temperature, until) + ) async def async_set_hvac_mode(self, hvac_mode: str) -> None: - """Set an operating mode for the Zone.""" - if hvac_mode == HVAC_MODE_OFF: - await self._set_temperature(self.min_temp, until=None) + """Set a Zone to one of its native EVO_* operating modes. + + Zones inherit their _effective_ operating mode from their Controller. + Usually, Zones are in 'FollowSchedule' mode, where their setpoints are a + function of their own schedule and the Controller's operating mode, e.g. + 'AutoWithEco' mode means their setpoint is (by default) 3C less than scheduled. + + However, Zones can _override_ these setpoints, either indefinitely, + 'PermanentOverride' mode, or for a set period of time, 'TemporaryOverride' mode + (after which they will revert back to 'FollowSchedule' mode). + + Finally, some of the Controller's operating modes are _forced_ upon the Zones, + regardless of any override mode, e.g. 'HeatingOff', Zones to (by default) 5C, + and 'Away', Zones to (by default) 12C. + """ + if hvac_mode == HVAC_MODE_OFF: + await self._call_client_api( + self._evo_device.set_temperature(self.min_temp, until=None) + ) else: # HVAC_MODE_HEAT - await self._set_zone_mode(EVO_FOLLOW) + await self._call_client_api(self._evo_device.cancel_temp_override()) async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: - """Set a new preset mode. + """Set the preset mode; if None, then revert to following the schedule.""" + evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW) - If preset_mode is None, then revert to following the schedule. - """ - await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) + if evo_preset_mode == EVO_FOLLOW: + await self._call_client_api(self._evo_device.cancel_temp_override()) + return + + temperature = self._evo_device.setpointStatus["targetHeatTemperature"] + + if evo_preset_mode == EVO_TEMPOVER: + await self._update_schedule() + until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + else: # EVO_PERMOVER + until = None + + await self._call_client_api( + self._evo_device.set_temperature(temperature, until) + ) + + async def async_update(self) -> None: + """Get the latest state data for a Zone.""" + await super().async_update() + + for attr in STATE_ATTRS_ZONES: + self._device_state_attrs[attr] = getattr(self._evo_device, attr) class EvoController(EvoClimateDevice): @@ -298,21 +280,20 @@ class EvoController(EvoClimateDevice): """ def __init__(self, evo_broker, evo_device) -> None: - """Initialize the evohome Controller (hub).""" + """Initialize a evohome Controller (hub).""" super().__init__(evo_broker, evo_device) + self._unique_id = evo_device.systemId self._name = evo_device.location.name self._icon = "mdi:thermostat" self._precision = PRECISION_TENTHS - self._state_attributes = ["systemId", "activeFaults", "systemModeStatus"] - self._supported_features = SUPPORT_PRESET_MODE self._preset_modes = list(HA_PRESET_TO_TCS) @property def hvac_mode(self) -> str: - """Return the current operating mode of the evohome Controller.""" + """Return the current operating mode of a Controller.""" tcs_mode = self._evo_tcs.systemModeStatus["mode"] return HVAC_MODE_OFF if tcs_mode == EVO_HEATOFF else HVAC_MODE_HEAT @@ -334,52 +315,53 @@ def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) - async def async_set_temperature(self, **kwargs) -> None: - """Do nothing. + @property + def min_temp(self) -> float: + """Return None as Controllers don't have a target temperature.""" + return None - The evohome Controller doesn't have a target temperature. - """ - return + @property + def max_temp(self) -> float: + """Return None as Controllers don't have a target temperature.""" + return None + + async def async_set_temperature(self, **kwargs) -> None: + """Raise exception as Controllers don't have a target temperature.""" + raise NotImplementedError("Evohome Controllers don't have target temperatures.") async def async_set_hvac_mode(self, hvac_mode: str) -> None: - """Set an operating mode for the Controller.""" + """Set an operating mode for a Controller.""" await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: - """Set a new preset mode. - - If preset_mode is None, then revert to 'Auto' mode. - """ + """Set the preset mode; if None, then revert to 'Auto' mode.""" await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) async def async_update(self) -> None: - """Get the latest state data.""" - return + """Get the latest state data for a Controller.""" + self._device_state_attrs = {} + + attrs = self._device_state_attrs + for attr in STATE_ATTRS_TCS: + if attr == "activeFaults": + attrs["activeSystemFaults"] = getattr(self._evo_tcs, attr) + else: + attrs[attr] = getattr(self._evo_tcs, attr) class EvoThermostat(EvoZone): """Base for a Honeywell Round Thermostat. - Implemented as a combined Controller/Zone. + These are implemented as a combined Controller/Zone. """ def __init__(self, evo_broker, evo_device) -> None: - """Initialize the Round Thermostat.""" + """Initialize the Thermostat.""" super().__init__(evo_broker, evo_device) self._name = evo_broker.tcs.location.name self._preset_modes = [PRESET_AWAY, PRESET_ECO] - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device-specific state attributes.""" - status = super().device_state_attributes["status"] - - status["systemModeStatus"] = self._evo_tcs.systemModeStatus - status["activeFaults"] += self._evo_tcs.activeFaults - - return {"status": status} - @property def hvac_mode(self) -> str: """Return the current operating mode.""" @@ -404,11 +386,19 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: - """Set a new preset mode. - - If preset_mode is None, then revert to following the schedule. - """ + """Set the preset mode; if None, then revert to following the schedule.""" if preset_mode in list(HA_PRESET_TO_TCS): await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode)) else: - await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) + await super().async_set_hvac_mode(preset_mode) + + async def async_update(self) -> None: + """Get the latest state data for the Thermostat.""" + await super().async_update() + + attrs = self._device_state_attrs + for attr in STATE_ATTRS_TCS: + if attr == "activeFaults": # self._evo_device also has "activeFaults" + attrs["activeSystemFaults"] = getattr(self._evo_tcs, attr) + else: + attrs[attr] = getattr(self._evo_tcs, attr) diff --git a/homeassistant/components/evohome/const.py b/homeassistant/components/evohome/const.py index a0480d62a10aa2..444671cf82aa8f 100644 --- a/homeassistant/components/evohome/const.py +++ b/homeassistant/components/evohome/const.py @@ -21,5 +21,3 @@ # These are used only to help prevent E501 (line too long) violations GWS = "gateways" TCS = "temperatureControlSystems" - -EVO_STRFTIME = "%Y-%m-%dT%H:%M:%SZ" diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 1b37bc3b2b58c2..b65665eb2c9ad2 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -3,27 +3,31 @@ from typing import List from homeassistant.components.water_heater import ( + SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, WaterHeaterDevice, ) from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime -from . import EvoDevice -from .const import DOMAIN, EVO_STRFTIME, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER +from . import EvoChild +from .const import DOMAIN, EVO_FOLLOW, EVO_PERMOVER _LOGGER = logging.getLogger(__name__) -HA_STATE_TO_EVO = {STATE_ON: "On", STATE_OFF: "Off"} -EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items()} +STATE_AUTO = "auto" -HA_OPMODE_TO_DHW = {STATE_ON: EVO_FOLLOW, STATE_OFF: EVO_PERMOVER} +HA_STATE_TO_EVO = {STATE_AUTO: "", STATE_ON: "On", STATE_OFF: "Off"} +EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items() if k != ""} + +STATE_ATTRS_DHW = ["dhwId", "activeFaults", "stateStatus", "temperatureStatus"] async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ) -> None: - """Create the DHW controller.""" + """Create a DHW controller.""" if discovery_info is None: return @@ -38,63 +42,71 @@ async def async_setup_platform( async_add_entities([evo_dhw], update_before_add=True) -class EvoDHW(EvoDevice, WaterHeaterDevice): +class EvoDHW(EvoChild, WaterHeaterDevice): """Base for a Honeywell evohome DHW controller (aka boiler).""" def __init__(self, evo_broker, evo_device) -> None: - """Initialize the evohome DHW controller.""" + """Initialize a evohome DHW controller.""" super().__init__(evo_broker, evo_device) + self._unique_id = evo_device.dhwId self._name = "DHW controller" self._icon = "mdi:thermometer-lines" self._precision = PRECISION_WHOLE - self._state_attributes = [ - "dhwId", - "activeFaults", - "stateStatus", - "temperatureStatus", - "setpoints", - ] - - self._supported_features = SUPPORT_OPERATION_MODE - self._operation_list = list(HA_OPMODE_TO_DHW) + self._supported_features = SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._evo_device.temperatureStatus.get("isAvailable", False) + def state(self): + """Return the current state.""" + return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] @property def current_operation(self) -> str: - """Return the current operating mode (On, or Off).""" + """Return the current operating mode (Auto, On, or Off).""" + if self._evo_device.stateStatus["mode"] == EVO_FOLLOW: + return STATE_AUTO return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] @property def operation_list(self) -> List[str]: """Return the list of available operations.""" - return self._operation_list + return list(HA_STATE_TO_EVO) @property - def current_temperature(self) -> float: - """Return the current temperature.""" - return self._evo_device.temperatureStatus["temperature"] + def is_away_mode_on(self): + """Return True if away mode is on.""" + is_off = EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] == STATE_OFF + is_permanent = self._evo_device.stateStatus["mode"] == EVO_PERMOVER + return is_off and is_permanent async def async_set_operation_mode(self, operation_mode: str) -> None: - """Set new operation mode for a DHW controller.""" - op_mode = HA_OPMODE_TO_DHW[operation_mode] - - state = "" if op_mode == EVO_FOLLOW else HA_STATE_TO_EVO[STATE_OFF] - until = None # EVO_FOLLOW, EVO_PERMOVER + """Set new operation mode for a DHW controller. - if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]: + Except for Auto, the mode is only until the next SetPoint. + """ + if operation_mode == STATE_AUTO: + await self._call_client_api(self._evo_device.set_dhw_auto()) + else: await self._update_schedule() - if self._schedule["DailySchedules"]: - until = parse_datetime(self.setpoints["next"]["from"]) - until = until.strftime(EVO_STRFTIME) + until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + + if operation_mode == STATE_ON: + await self._call_client_api(self._evo_device.set_dhw_on(until)) + else: # STATE_OFF + await self._call_client_api(self._evo_device.set_dhw_off(until)) + + async def async_turn_away_mode_on(self): + """Turn away mode on.""" + await self._call_client_api(self._evo_device.set_dhw_off()) + + async def async_turn_away_mode_off(self): + """Turn away mode off.""" + await self._call_client_api(self._evo_device.set_dhw_auto()) - data = {"Mode": op_mode, "State": state, "UntilTime": until} + async def async_update(self) -> None: + """Get the latest state data for a DHW controller.""" + await super().async_update() - await self._call_client_api( - self._evo_device._set_dhw(data) # pylint: disable=protected-access - ) + for attr in STATE_ATTRS_DHW: + self._device_state_attrs[attr] = getattr(self._evo_device, attr) From 2e3bc5964dc4cbbdc7fd78b1aaa10fa7cfe660b9 Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Tue, 1 Oct 2019 13:25:57 +0200 Subject: [PATCH 232/296] Add saj component (#26902) * Add saj component * Performed requested changes after review * Performed requested changes after review 2 * Performed requested changes after review 3 * Black * Bump pysaj library version * Changes after review * Fix flake8 * Review changes + isort --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/saj/__init__.py | 1 + homeassistant/components/saj/manifest.json | 12 ++ homeassistant/components/saj/sensor.py | 202 +++++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 220 insertions(+) create mode 100644 homeassistant/components/saj/__init__.py create mode 100644 homeassistant/components/saj/manifest.json create mode 100644 homeassistant/components/saj/sensor.py diff --git a/.coveragerc b/.coveragerc index f28e9aaeda67e2..aa8f2d8c03d16e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -558,6 +558,7 @@ omit = homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py homeassistant/components/sabnzbd/* + homeassistant/components/saj/sensor.py homeassistant/components/satel_integra/* homeassistant/components/scrape/sensor.py homeassistant/components/scsgate/* diff --git a/CODEOWNERS b/CODEOWNERS index db0ff3226c38ba..f5cd03882c5944 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -235,6 +235,7 @@ homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt +homeassistant/components/saj/* @fredericvl homeassistant/components/scene/* @home-assistant/core homeassistant/components/scrape/* @fabaff homeassistant/components/script/* @home-assistant/core diff --git a/homeassistant/components/saj/__init__.py b/homeassistant/components/saj/__init__.py new file mode 100644 index 00000000000000..03277bba4df351 --- /dev/null +++ b/homeassistant/components/saj/__init__.py @@ -0,0 +1 @@ +"""The saj component.""" diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json new file mode 100644 index 00000000000000..c0367c47902089 --- /dev/null +++ b/homeassistant/components/saj/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "saj", + "name": "SAJ", + "documentation": "https://www.home-assistant.io/components/saj", + "requirements": [ + "pysaj==0.0.9" + ], + "dependencies": [], + "codeowners": [ + "@fredericvl" + ] +} diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py new file mode 100644 index 00000000000000..fa06b2b9125ec7 --- /dev/null +++ b/homeassistant/components/saj/sensor.py @@ -0,0 +1,202 @@ +"""SAJ solar inverter interface.""" +import asyncio +from datetime import date +import logging + +import pysaj +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + MASS_KILOGRAMS, + POWER_WATT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import CALLBACK_TYPE, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_call_later + +_LOGGER = logging.getLogger(__name__) + +MIN_INTERVAL = 5 +MAX_INTERVAL = 300 + +UNIT_OF_MEASUREMENT_HOURS = "h" + +SAJ_UNIT_MAPPINGS = { + "W": POWER_WATT, + "kWh": ENERGY_KILO_WATT_HOUR, + "h": UNIT_OF_MEASUREMENT_HOURS, + "kg": MASS_KILOGRAMS, + "°C": TEMP_CELSIUS, + "": None, +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up SAJ sensors.""" + + remove_interval_update = None + + # Init all sensors + sensor_def = pysaj.Sensors() + + # Use all sensors by default + hass_sensors = [] + + for sensor in sensor_def: + hass_sensors.append(SAJsensor(sensor)) + + saj = pysaj.SAJ(config[CONF_HOST]) + + async_add_entities(hass_sensors) + + async def async_saj(): + """Update all the SAJ sensors.""" + tasks = [] + + values = await saj.read(sensor_def) + + for sensor in hass_sensors: + state_unknown = False + if not values: + # SAJ inverters are powered by DC via solar panels and thus are + # offline after the sun has set. If a sensor resets on a daily + # basis like "today_yield", this reset won't happen automatically. + # Code below checks if today > day when sensor was last updated + # and if so: set state to None. + # Sensors with live values like "temperature" or "current_power" + # will also be reset to None. + if (sensor.per_day_basis and date.today() > sensor.date_updated) or ( + not sensor.per_day_basis and not sensor.per_total_basis + ): + state_unknown = True + task = sensor.async_update_values(unknown_state=state_unknown) + if task: + tasks.append(task) + if tasks: + await asyncio.wait(tasks) + return values + + def start_update_interval(event): + """Start the update interval scheduling.""" + nonlocal remove_interval_update + remove_interval_update = async_track_time_interval_backoff(hass, async_saj) + + def stop_update_interval(event): + """Properly cancel the scheduled update.""" + remove_interval_update() # pylint: disable=not-callable + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_update_interval) + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, stop_update_interval) + + +@callback +def async_track_time_interval_backoff(hass, action) -> CALLBACK_TYPE: + """Add a listener that fires repetitively and increases the interval when failed.""" + remove = None + interval = MIN_INTERVAL + + async def interval_listener(now=None): + """Handle elapsed interval with backoff.""" + nonlocal interval, remove + try: + if await action(): + interval = MIN_INTERVAL + else: + interval = min(interval * 2, MAX_INTERVAL) + finally: + remove = async_call_later(hass, interval, interval_listener) + + hass.async_create_task(interval_listener()) + + def remove_listener(): + """Remove interval listener.""" + if remove: + remove() # pylint: disable=not-callable + + return remove_listener + + +class SAJsensor(Entity): + """Representation of a SAJ sensor.""" + + def __init__(self, pysaj_sensor): + """Initialize the sensor.""" + self._sensor = pysaj_sensor + self._state = self._sensor.value + + @property + def name(self): + """Return the name of the sensor.""" + return f"saj_{self._sensor.name}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SAJ_UNIT_MAPPINGS[self._sensor.unit] + + @property + def device_class(self): + """Return the device class the sensor belongs to.""" + if self.unit_of_measurement == POWER_WATT: + return DEVICE_CLASS_POWER + if ( + self.unit_of_measurement == TEMP_CELSIUS + or self._sensor.unit == TEMP_FAHRENHEIT + ): + return DEVICE_CLASS_TEMPERATURE + + @property + def should_poll(self) -> bool: + """SAJ sensors are updated & don't poll.""" + return False + + @property + def per_day_basis(self) -> bool: + """Return if the sensors value is on daily basis or not.""" + return self._sensor.per_day_basis + + @property + def per_total_basis(self) -> bool: + """Return if the sensors value is cummulative or not.""" + return self._sensor.per_total_basis + + @property + def date_updated(self) -> date: + """Return the date when the sensor was last updated.""" + return self._sensor.date + + def async_update_values(self, unknown_state=False): + """Update this sensor.""" + update = False + + if self._sensor.value != self._state: + update = True + self._state = self._sensor.value + + if unknown_state and self._state is not None: + update = True + self._state = None + + return self.async_update_ha_state() if update else None + + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return f"{self._sensor.name}" diff --git a/requirements_all.txt b/requirements_all.txt index 561e3345417251..ef1b56222b5c9c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1410,6 +1410,9 @@ pyrepetier==3.0.5 # homeassistant.components.sabnzbd pysabnzbd==1.1.0 +# homeassistant.components.saj +pysaj==0.0.9 + # homeassistant.components.sony_projector pysdcp==1 From f4a1f2809bdbcd74dd1bf6476467f474912ed041 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Tue, 1 Oct 2019 22:15:15 +1000 Subject: [PATCH 233/296] Add availability_template to Template Lock platform (#26517) * Added availability_template to Template Lock platform * Added to test for invalid values in availability_template * Black and Lint fix * black formatting * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Moved availability_template rendering to common loop * Brought contant into line * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (async, magic values and state checks) --- homeassistant/components/template/lock.py | 40 ++++++++++++- tests/components/template/test_lock.py | 70 ++++++++++++++++++++++- 2 files changed, 104 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index d7f501987f9527..aa8cc8b1224e3f 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -19,6 +19,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -34,6 +35,7 @@ vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA, vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA, vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, } ) @@ -48,21 +50,32 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=N if value_template_entity_ids == MATCH_ALL: _LOGGER.warning( - "Template lock %s has no entity ids configured to track nor " - "were we able to extract the entities to track from the %s " + "Template lock '%s' has no entity ids configured to track nor " + "were we able to extract the entities to track from the '%s' " "template. This entity will only be able to be updated " "manually.", name, CONF_VALUE_TEMPLATE, ) + template_entity_ids = set() + template_entity_ids |= set(value_template_entity_ids) + + availability_template = config.get(CONF_AVAILABILITY_TEMPLATE) + if availability_template is not None: + availability_template.hass = hass + temp_ids = availability_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + async_add_devices( [ TemplateLock( hass, name, value_template, - value_template_entity_ids, + availability_template, + template_entity_ids, config.get(CONF_LOCK), config.get(CONF_UNLOCK), config.get(CONF_OPTIMISTIC), @@ -79,6 +92,7 @@ def __init__( hass, name, value_template, + availability_template, entity_ids, command_lock, command_unlock, @@ -89,10 +103,12 @@ def __init__( self._hass = hass self._name = name self._state_template = value_template + self._availability_template = availability_template self._state_entities = entity_ids self._command_lock = Script(hass, command_lock) self._command_unlock = Script(hass, command_unlock) self._optimistic = optimistic + self._available = True async def async_added_to_hass(self): """Register callbacks.""" @@ -136,6 +152,11 @@ def is_locked(self): """Return true if lock is locked.""" return self._state + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_update(self): """Update the state from the template.""" try: @@ -148,6 +169,19 @@ async def async_update(self): self._state = None _LOGGER.error("Could not render template %s: %s", self._name, ex) + if self._availability_template is not None: + try: + self._available = ( + self._availability_template.async_render().lower() == "true" + ) + except (TemplateError, ValueError) as ex: + _LOGGER.error( + "Could not render %s template %s: %s", + CONF_AVAILABILITY_TEMPLATE, + self._name, + ex, + ) + async def async_lock(self, **kwargs): """Lock the device.""" if self._optimistic: diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 24cde24051a69a..d1d3020737549b 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -5,7 +5,7 @@ from homeassistant import setup from homeassistant.components import lock from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from tests.common import get_test_home_assistant, assert_setup_component @@ -254,9 +254,9 @@ def test_no_template_match_all(self, caplog): assert state.state == lock.STATE_UNLOCKED assert ( - "Template lock Template Lock has no entity ids configured " + "Template lock 'Template Lock' has no entity ids configured " "to track nor were we able to extract the entities to track " - "from the value_template template. This entity will only " + "from the 'value_template' template. This entity will only " "be able to be updated manually." ) in caplog.text @@ -332,3 +332,67 @@ def test_unlock_action(self): self.hass.block_till_done() assert len(self.calls) == 1 + + +async def test_available_template_with_entities(hass): + """Test availability templates with values from other entities.""" + + await setup.async_setup_component( + hass, + "lock", + { + "lock": { + "platform": "template", + "value_template": "{{ 'on' }}", + "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, + "unlock": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + "availability_template": "{{ is_state('availability_state.state', 'on') }}", + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get("lock.template_lock").state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get("lock.template_lock").state == STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + await setup.async_setup_component( + hass, + "lock", + { + "lock": { + "platform": "template", + "value_template": "{{ 1 + 1 }}", + "availability_template": "{{ x - 12 }}", + "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, + "unlock": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("lock.template_lock").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text From c1851a2d94d724ceee0eab56249623fe2d92a606 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 1 Oct 2019 16:59:06 +0200 Subject: [PATCH 234/296] Cleanup coroutine threadsafe (#27080) * Cleanup coroutine threadsafe * fix lint * Fix typing * Fix tests * Fix black --- .../bluetooth_le_tracker/device_tracker.py | 4 +- homeassistant/components/generic/camera.py | 3 +- homeassistant/components/group/__init__.py | 5 +- homeassistant/components/mqtt/__init__.py | 4 +- homeassistant/components/proxy/camera.py | 3 +- homeassistant/core.py | 12 +- homeassistant/helpers/entity_platform.py | 4 +- homeassistant/helpers/script.py | 6 +- homeassistant/helpers/service.py | 5 +- homeassistant/helpers/state.py | 3 +- homeassistant/setup.py | 3 +- homeassistant/util/async_.py | 16 +-- homeassistant/util/logging.py | 6 +- tests/common.py | 8 +- tests/components/camera/test_init.py | 9 +- tests/components/group/test_notify.py | 6 +- tests/components/homeassistant/test_init.py | 4 +- .../media_player/test_async_helpers.py | 41 ++++--- tests/components/rest/test_switch.py | 49 +++++--- .../components/universal/test_media_player.py | 116 ++++++++++-------- tests/components/uptime/test_sensor.py | 26 ++-- tests/test_core.py | 15 ++- tests/util/test_async.py | 80 ------------ 23 files changed, 196 insertions(+), 232 deletions(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 8cba3032f54cb4..29eecdfd07741c 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -1,4 +1,5 @@ """Tracking for bluetooth low energy devices.""" +import asyncio import logging from homeassistant.helpers.event import track_point_in_utc_time @@ -14,7 +15,6 @@ ) from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.util.dt as dt_util -from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -89,7 +89,7 @@ def discover_ble_devices(): # Load all known devices. # We just need the devices so set consider_home and home range # to 0 - for device in run_coroutine_threadsafe( + for device in asyncio.run_coroutine_threadsafe( async_load_config(yaml_path, hass, 0), hass.loop ).result(): # check if device is a valid bluetooth device diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 307142ed98964f..01d2fb948eda1e 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -26,7 +26,6 @@ ) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv -from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -105,7 +104,7 @@ def frame_interval(self): def camera_image(self): """Return bytes of camera image.""" - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( self.async_camera_image(), self.hass.loop ).result() diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 204fcab0381aeb..39574a2b03b7e2 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -34,7 +34,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.util.async_ import run_coroutine_threadsafe # mypy: allow-untyped-calls, allow-untyped-defs @@ -430,7 +429,7 @@ def create_group( mode=None, ): """Initialize a group.""" - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( Group.async_create_group( hass, name, @@ -546,7 +545,7 @@ def assumed_state(self): def update_tracked_entity_ids(self, entity_ids): """Update the member entity IDs.""" - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.async_update_tracked_entity_ids(entity_ids), self.hass.loop ).result() diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 8d83cd0cc2ba0f..9b25a6ef6e4817 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -39,7 +39,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.loader import bind_hass -from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception # Loading the config flow file will register the flow @@ -463,7 +463,7 @@ def subscribe( encoding: str = "utf-8", ) -> Callable[[], None]: """Subscribe to an MQTT topic.""" - async_remove = run_coroutine_threadsafe( + async_remove = asyncio.run_coroutine_threadsafe( async_subscribe(hass, topic, msg_callback, qos, encoding), hass.loop ).result() diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 53a4f620dcc3d4..b1ce8ad7ac0466 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -9,7 +9,6 @@ from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -220,7 +219,7 @@ def __init__(self, hass, config): def camera_image(self): """Return camera image.""" - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( self.async_camera_image(), self.hass.loop ).result() diff --git a/homeassistant/core.py b/homeassistant/core.py index f4be3b66323d1b..feb4445d36d5bd 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -64,11 +64,7 @@ Unauthorized, ServiceNotFound, ) -from homeassistant.util.async_ import ( - run_coroutine_threadsafe, - run_callback_threadsafe, - fire_coroutine_threadsafe, -) +from homeassistant.util.async_ import run_callback_threadsafe, fire_coroutine_threadsafe from homeassistant import util import homeassistant.util.dt as dt_util from homeassistant.util import location, slugify @@ -375,7 +371,9 @@ def async_run_job(self, target: Callable[..., None], *args: Any) -> None: def block_till_done(self) -> None: """Block till all pending work is done.""" - run_coroutine_threadsafe(self.async_block_till_done(), self.loop).result() + asyncio.run_coroutine_threadsafe( + self.async_block_till_done(), self.loop + ).result() async def async_block_till_done(self) -> None: """Block till all pending work is done.""" @@ -1168,7 +1166,7 @@ def call( Because the service is sent as an event you are not allowed to use the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. """ - return run_coroutine_threadsafe( # type: ignore + return asyncio.run_coroutine_threadsafe( self.async_call(domain, service, service_data, blocking, context), self._hass.loop, ).result() diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 7d5debd484d4e8..5c59dc6c13e3a0 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -6,7 +6,7 @@ from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import callback, valid_entity_id, split_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe from .entity_registry import DISABLED_INTEGRATION from .event import async_track_time_interval, async_call_later @@ -220,7 +220,7 @@ def add_entities(self, new_entities, update_before_add=False): "only inside tests or you can run into a deadlock!" ) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.async_add_entities(list(new_entities), update_before_add), self.hass.loop, ).result() diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 14ff873d4d173b..a4f6afa16366af 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,5 +1,5 @@ """Helpers to execute scripts.""" - +import asyncio import logging from contextlib import suppress from datetime import datetime @@ -29,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration import homeassistant.util.dt as date_util -from homeassistant.util.async_ import run_coroutine_threadsafe, run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -136,7 +136,7 @@ def is_running(self) -> bool: def run(self, variables=None, context=None): """Run script.""" - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.async_run(variables, context), self.hass.loop ).result() diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index f29d1885d1e145..e177c86c65c971 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -20,7 +20,6 @@ from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml.loader import JSON_TYPE import homeassistant.helpers.config_validation as cv -from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.helpers.typing import HomeAssistantType @@ -42,7 +41,7 @@ def call_from_config( hass, config, blocking=False, variables=None, validate_config=True ): """Call a service based on a config hash.""" - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( async_call_from_config(hass, config, blocking, variables, validate_config), hass.loop, ).result() @@ -105,7 +104,7 @@ def extract_entity_ids(hass, service_call, expand_group=True): Will convert group entity ids to the entity ids it represents. """ - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( async_extract_entity_ids(hass, service_call, expand_group), hass.loop ).result() diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 7f9692b3380264..2f49a566a32e5e 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -44,7 +44,6 @@ SERVICE_SELECT_OPTION, ) from homeassistant.core import Context, State, DOMAIN as HASS_DOMAIN -from homeassistant.util.async_ import run_coroutine_threadsafe from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -122,7 +121,7 @@ def reproduce_state( blocking: bool = False, ) -> None: """Reproduce given state.""" - return run_coroutine_threadsafe( # type: ignore + return asyncio.run_coroutine_threadsafe( async_reproduce_state(hass, states, blocking), hass.loop ).result() diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 58e4fc19eb0ee8..07de3b2942d061 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -10,7 +10,6 @@ from homeassistant.config import async_notify_setup_error from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -25,7 +24,7 @@ def setup_component(hass: core.HomeAssistant, domain: str, config: Dict) -> bool: """Set up a component and all its dependencies.""" - return run_coroutine_threadsafe( # type: ignore + return asyncio.run_coroutine_threadsafe( async_setup_component(hass, domain, config), hass.loop ).result() diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 6920e0d97f64cd..64bedfe2501676 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -7,7 +7,7 @@ import asyncio from asyncio import ensure_future -from typing import Any, Union, Coroutine, Callable, Generator, TypeVar, Awaitable +from typing import Any, Coroutine, Callable, TypeVar, Awaitable _LOGGER = logging.getLogger(__name__) @@ -30,20 +30,6 @@ def asyncio_run(main: Awaitable[_T], *, debug: bool = False) -> _T: loop.close() -def run_coroutine_threadsafe( - coro: Union[Coroutine, Generator], loop: AbstractEventLoop -) -> concurrent.futures.Future: - """Submit a coroutine object to a given event loop. - - Return a concurrent.futures.Future to access the result. - """ - ident = loop.__dict__.get("_thread_ident") - if ident is not None and ident == threading.get_ident(): - raise RuntimeError("Cannot be called from within the event loop") - - return asyncio.run_coroutine_threadsafe(coro, loop) - - def fire_coroutine_threadsafe(coro: Coroutine, loop: AbstractEventLoop) -> None: """Submit a coroutine object to a given event loop. diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 79cb2607b1045f..99e606d28662ef 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -8,8 +8,6 @@ import traceback from typing import Any, Callable, Coroutine, Optional -from .async_ import run_coroutine_threadsafe - class HideSensitiveDataFilter(logging.Filter): """Filter API password calls.""" @@ -83,7 +81,9 @@ def __repr__(self) -> str: def _process(self) -> None: """Process log in a thread.""" while True: - record = run_coroutine_threadsafe(self._queue.get(), self.loop).result() + record = asyncio.run_coroutine_threadsafe( + self._queue.get(), self.loop + ).result() if record is None: self.handler.close() diff --git a/tests/common.py b/tests/common.py index bc39b1f5e0b62a..1982e80dfe949e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -53,7 +53,7 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.unit_system import METRIC_SYSTEM -from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.components.device_automation import ( # noqa _async_get_device_automations as async_get_device_automations, ) @@ -92,7 +92,9 @@ def threadsafe_coroutine_factory(func): def threadsafe(*args, **kwargs): """Call func threadsafe.""" hass = args[0] - return run_coroutine_threadsafe(func(*args, **kwargs), hass.loop).result() + return asyncio.run_coroutine_threadsafe( + func(*args, **kwargs), hass.loop + ).result() return threadsafe @@ -125,7 +127,7 @@ def run_loop(): def start_hass(*mocks): """Start hass.""" - run_coroutine_threadsafe(hass.async_start(), loop).result() + asyncio.run_coroutine_threadsafe(hass.async_start(), loop).result() def stop_hass(): """Stop hass.""" diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 09793b4303e4c8..17bcaadb92b6ab 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -17,7 +17,6 @@ from homeassistant.components.camera.prefs import CameraEntityPreferences from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, @@ -110,7 +109,7 @@ def test_get_image_from_camera(self, mock_camera): """Grab an image from camera entity.""" self.hass.start() - image = run_coroutine_threadsafe( + image = asyncio.run_coroutine_threadsafe( camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop ).result() @@ -123,7 +122,7 @@ def test_get_image_without_exists_camera(self): "homeassistant.helpers.entity_component.EntityComponent." "get_entity", return_value=None, ), pytest.raises(HomeAssistantError): - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop ).result() @@ -133,7 +132,7 @@ def test_get_image_with_timeout(self): "homeassistant.components.camera.Camera.async_camera_image", side_effect=asyncio.TimeoutError, ), pytest.raises(HomeAssistantError): - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop ).result() @@ -143,7 +142,7 @@ def test_get_image_fails(self): "homeassistant.components.camera.Camera.async_camera_image", return_value=mock_coro(None), ), pytest.raises(HomeAssistantError): - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop ).result() diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index 75a151d7868b8f..d7b7496573bebd 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -1,4 +1,5 @@ """The tests for the notify.group platform.""" +import asyncio import unittest from unittest.mock import MagicMock, patch @@ -6,7 +7,6 @@ import homeassistant.components.notify as notify import homeassistant.components.group.notify as group import homeassistant.components.demo.notify as demo -from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import assert_setup_component, get_test_home_assistant @@ -43,7 +43,7 @@ def mock_get_service(hass, config, discovery_info=None): }, ) - self.service = run_coroutine_threadsafe( + self.service = asyncio.run_coroutine_threadsafe( group.async_get_service( self.hass, { @@ -70,7 +70,7 @@ def tearDown(self): # pylint: disable=invalid-name def test_send_message_with_data(self): """Test sending a message with to a notify group.""" - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.service.async_send_message( "Hello", title="Test notification", data={"hello": "world"} ), diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index e6f05cc2be098d..7a97de0f68e42f 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -1,5 +1,6 @@ """The tests for Core components.""" # pylint: disable=protected-access +import asyncio import unittest from unittest.mock import patch, Mock @@ -27,7 +28,6 @@ import homeassistant.helpers.intent as intent from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity -from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, @@ -111,7 +111,7 @@ class TestComponentsCore(unittest.TestCase): def setUp(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - assert run_coroutine_threadsafe( + assert asyncio.run_coroutine_threadsafe( async_setup_component(self.hass, "homeassistant", {}), self.hass.loop ).result() diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index a12b9af0ebd3d0..4a2e4fed6c565d 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -10,7 +10,6 @@ STATE_OFF, STATE_IDLE, ) -from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import get_test_home_assistant @@ -162,21 +161,23 @@ def tearDown(self): def test_volume_up(self): """Test the volume_up helper function.""" assert self.player.volume_level == 0 - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_set_volume_level(0.5), self.hass.loop ).result() assert self.player.volume_level == 0.5 - run_coroutine_threadsafe(self.player.async_volume_up(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.player.async_volume_up(), self.hass.loop + ).result() assert self.player.volume_level == 0.6 def test_volume_down(self): """Test the volume_down helper function.""" assert self.player.volume_level == 0 - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_set_volume_level(0.5), self.hass.loop ).result() assert self.player.volume_level == 0.5 - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_volume_down(), self.hass.loop ).result() assert self.player.volume_level == 0.4 @@ -184,11 +185,11 @@ def test_volume_down(self): def test_media_play_pause(self): """Test the media_play_pause helper function.""" assert self.player.state == STATE_OFF - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_media_play_pause(), self.hass.loop ).result() assert self.player.state == STATE_PLAYING - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_media_play_pause(), self.hass.loop ).result() assert self.player.state == STATE_PAUSED @@ -196,9 +197,13 @@ def test_media_play_pause(self): def test_toggle(self): """Test the toggle helper function.""" assert self.player.state == STATE_OFF - run_coroutine_threadsafe(self.player.async_toggle(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.player.async_toggle(), self.hass.loop + ).result() assert self.player.state == STATE_ON - run_coroutine_threadsafe(self.player.async_toggle(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.player.async_toggle(), self.hass.loop + ).result() assert self.player.state == STATE_OFF @@ -219,7 +224,9 @@ def test_volume_up(self): assert self.player.volume_level == 0 self.player.set_volume_level(0.5) assert self.player.volume_level == 0.5 - run_coroutine_threadsafe(self.player.async_volume_up(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.player.async_volume_up(), self.hass.loop + ).result() assert self.player.volume_level == 0.7 def test_volume_down(self): @@ -227,7 +234,7 @@ def test_volume_down(self): assert self.player.volume_level == 0 self.player.set_volume_level(0.5) assert self.player.volume_level == 0.5 - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_volume_down(), self.hass.loop ).result() assert self.player.volume_level == 0.3 @@ -235,11 +242,11 @@ def test_volume_down(self): def test_media_play_pause(self): """Test the media_play_pause helper function.""" assert self.player.state == STATE_OFF - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_media_play_pause(), self.hass.loop ).result() assert self.player.state == STATE_PLAYING - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_media_play_pause(), self.hass.loop ).result() assert self.player.state == STATE_PAUSED @@ -247,7 +254,11 @@ def test_media_play_pause(self): def test_toggle(self): """Test the toggle helper function.""" assert self.player.state == STATE_OFF - run_coroutine_threadsafe(self.player.async_toggle(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.player.async_toggle(), self.hass.loop + ).result() assert self.player.state == STATE_ON - run_coroutine_threadsafe(self.player.async_toggle(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.player.async_toggle(), self.hass.loop + ).result() assert self.player.state == STATE_OFF diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index ced2f512b49651..81430cff349db4 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -5,7 +5,6 @@ import homeassistant.components.rest.switch as rest from homeassistant.setup import setup_component -from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.helpers.template import Template from tests.common import get_test_home_assistant, assert_setup_component @@ -23,14 +22,14 @@ def teardown_method(self): def test_setup_missing_config(self): """Test setup with configuration missing required entries.""" - assert not run_coroutine_threadsafe( + assert not asyncio.run_coroutine_threadsafe( rest.async_setup_platform(self.hass, {"platform": "rest"}, None), self.hass.loop, ).result() def test_setup_missing_schema(self): """Test setup with resource missing schema.""" - assert not run_coroutine_threadsafe( + assert not asyncio.run_coroutine_threadsafe( rest.async_setup_platform( self.hass, {"platform": "rest", "resource": "localhost"}, None ), @@ -40,7 +39,7 @@ def test_setup_missing_schema(self): def test_setup_failed_connect(self, aioclient_mock): """Test setup when connection error occurs.""" aioclient_mock.get("http://localhost", exc=aiohttp.ClientError) - assert not run_coroutine_threadsafe( + assert not asyncio.run_coroutine_threadsafe( rest.async_setup_platform( self.hass, {"platform": "rest", "resource": "http://localhost"}, None ), @@ -50,7 +49,7 @@ def test_setup_failed_connect(self, aioclient_mock): def test_setup_timeout(self, aioclient_mock): """Test setup when connection timeout occurs.""" aioclient_mock.get("http://localhost", exc=asyncio.TimeoutError()) - assert not run_coroutine_threadsafe( + assert not asyncio.run_coroutine_threadsafe( rest.async_setup_platform( self.hass, {"platform": "rest", "resource": "http://localhost"}, None ), @@ -131,7 +130,9 @@ def test_is_on_before_update(self): def test_turn_on_success(self, aioclient_mock): """Test turn_on.""" aioclient_mock.post(self.resource, status=200) - run_coroutine_threadsafe(self.switch.async_turn_on(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_turn_on(), self.hass.loop + ).result() assert self.body_on.template == aioclient_mock.mock_calls[-1][2].decode() assert self.switch.is_on @@ -139,7 +140,9 @@ def test_turn_on_success(self, aioclient_mock): def test_turn_on_status_not_ok(self, aioclient_mock): """Test turn_on when error status returned.""" aioclient_mock.post(self.resource, status=500) - run_coroutine_threadsafe(self.switch.async_turn_on(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_turn_on(), self.hass.loop + ).result() assert self.body_on.template == aioclient_mock.mock_calls[-1][2].decode() assert self.switch.is_on is None @@ -147,14 +150,18 @@ def test_turn_on_status_not_ok(self, aioclient_mock): def test_turn_on_timeout(self, aioclient_mock): """Test turn_on when timeout occurs.""" aioclient_mock.post(self.resource, status=500) - run_coroutine_threadsafe(self.switch.async_turn_on(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_turn_on(), self.hass.loop + ).result() assert self.switch.is_on is None def test_turn_off_success(self, aioclient_mock): """Test turn_off.""" aioclient_mock.post(self.resource, status=200) - run_coroutine_threadsafe(self.switch.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_turn_off(), self.hass.loop + ).result() assert self.body_off.template == aioclient_mock.mock_calls[-1][2].decode() assert not self.switch.is_on @@ -162,7 +169,9 @@ def test_turn_off_success(self, aioclient_mock): def test_turn_off_status_not_ok(self, aioclient_mock): """Test turn_off when error status returned.""" aioclient_mock.post(self.resource, status=500) - run_coroutine_threadsafe(self.switch.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_turn_off(), self.hass.loop + ).result() assert self.body_off.template == aioclient_mock.mock_calls[-1][2].decode() assert self.switch.is_on is None @@ -170,34 +179,44 @@ def test_turn_off_status_not_ok(self, aioclient_mock): def test_turn_off_timeout(self, aioclient_mock): """Test turn_off when timeout occurs.""" aioclient_mock.post(self.resource, exc=asyncio.TimeoutError()) - run_coroutine_threadsafe(self.switch.async_turn_on(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_turn_on(), self.hass.loop + ).result() assert self.switch.is_on is None def test_update_when_on(self, aioclient_mock): """Test update when switch is on.""" aioclient_mock.get(self.resource, text=self.body_on.template) - run_coroutine_threadsafe(self.switch.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_update(), self.hass.loop + ).result() assert self.switch.is_on def test_update_when_off(self, aioclient_mock): """Test update when switch is off.""" aioclient_mock.get(self.resource, text=self.body_off.template) - run_coroutine_threadsafe(self.switch.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_update(), self.hass.loop + ).result() assert not self.switch.is_on def test_update_when_unknown(self, aioclient_mock): """Test update when unknown status returned.""" aioclient_mock.get(self.resource, text="unknown status") - run_coroutine_threadsafe(self.switch.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_update(), self.hass.loop + ).result() assert self.switch.is_on is None def test_update_timeout(self, aioclient_mock): """Test update when timeout occurs.""" aioclient_mock.get(self.resource, exc=asyncio.TimeoutError()) - run_coroutine_threadsafe(self.switch.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_update(), self.hass.loop + ).result() assert self.switch.is_on is None diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index fd6c0f73303988..67d826f576b5d7 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the Universal Media player platform.""" +import asyncio from copy import copy import unittest @@ -10,7 +11,6 @@ import homeassistant.components.input_select as input_select import homeassistant.components.media_player as media_player import homeassistant.components.universal.media_player as universal -from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import mock_service, get_test_home_assistant @@ -298,7 +298,7 @@ def add_entities(new_entities): setup_ok = True try: - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( universal.async_setup_platform( self.hass, validate_config(bad_config), add_entities ), @@ -309,7 +309,7 @@ def add_entities(new_entities): assert not setup_ok assert 0 == len(entities) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( universal.async_setup_platform( self.hass, validate_config(config), add_entities ), @@ -369,26 +369,26 @@ def test_active_child_state(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump._child_state is None self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert self.mock_mp_1.entity_id == ump._child_state.entity_id self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert self.mock_mp_1.entity_id == ump._child_state.entity_id self.mock_mp_1._state = STATE_OFF self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert self.mock_mp_2.entity_id == ump._child_state.entity_id def test_name(self): @@ -413,14 +413,14 @@ def test_state_children_only(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.state, STATE_OFF self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_PLAYING == ump.state def test_state_with_children_and_attrs(self): @@ -429,22 +429,22 @@ def test_state_with_children_and_attrs(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_OFF == ump.state self.hass.states.set(self.mock_state_switch_id, STATE_ON) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_ON == ump.state self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_PLAYING == ump.state self.hass.states.set(self.mock_state_switch_id, STATE_OFF) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_OFF == ump.state def test_volume_level(self): @@ -453,20 +453,20 @@ def test_volume_level(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.volume_level is None self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert 0 == ump.volume_level self.mock_mp_1._volume_level = 1 self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert 1 == ump.volume_level def test_media_image_url(self): @@ -476,7 +476,7 @@ def test_media_image_url(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.media_image_url is None @@ -484,7 +484,7 @@ def test_media_image_url(self): self.mock_mp_1._media_image_url = test_url self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() # mock_mp_1 will convert the url to the api proxy url. This test # ensures ump passes through the same url without an additional proxy. assert self.mock_mp_1.entity_picture == ump.entity_picture @@ -495,20 +495,20 @@ def test_is_volume_muted_children_only(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert not ump.is_volume_muted self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert not ump.is_volume_muted self.mock_mp_1._is_volume_muted = True self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.is_volume_muted def test_source_list_children_and_attr(self): @@ -561,7 +561,7 @@ def test_supported_features_children_only(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert 0 == ump.supported_features @@ -569,7 +569,7 @@ def test_supported_features_children_only(self): self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert 512 == ump.supported_features def test_supported_features_children_and_cmds(self): @@ -590,12 +590,12 @@ def test_supported_features_children_and_cmds(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() check_flags = ( universal.SUPPORT_TURN_ON @@ -615,16 +615,16 @@ def test_service_call_no_active_child(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_1._state = STATE_OFF self.mock_mp_1.schedule_update_ha_state() self.mock_mp_2._state = STATE_OFF self.mock_mp_2.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() assert 0 == len(self.mock_mp_1.service_calls["turn_off"]) assert 0 == len(self.mock_mp_2.service_calls["turn_off"]) @@ -634,67 +634,85 @@ def test_service_call_to_child(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() assert 1 == len(self.mock_mp_2.service_calls["turn_off"]) - run_coroutine_threadsafe(ump.async_turn_on(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_turn_on(), self.hass.loop).result() assert 1 == len(self.mock_mp_2.service_calls["turn_on"]) - run_coroutine_threadsafe(ump.async_mute_volume(True), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_mute_volume(True), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["mute_volume"]) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( ump.async_set_volume_level(0.5), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["set_volume_level"]) - run_coroutine_threadsafe(ump.async_media_play(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_play(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_play"]) - run_coroutine_threadsafe(ump.async_media_pause(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_pause(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_pause"]) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( ump.async_media_previous_track(), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["media_previous_track"]) - run_coroutine_threadsafe(ump.async_media_next_track(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_next_track(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_next_track"]) - run_coroutine_threadsafe(ump.async_media_seek(100), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_seek(100), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_seek"]) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( ump.async_play_media("movie", "batman"), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["play_media"]) - run_coroutine_threadsafe(ump.async_volume_up(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_volume_up(), self.hass.loop).result() assert 1 == len(self.mock_mp_2.service_calls["volume_up"]) - run_coroutine_threadsafe(ump.async_volume_down(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_volume_down(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["volume_down"]) - run_coroutine_threadsafe(ump.async_media_play_pause(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_play_pause(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_play_pause"]) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( ump.async_select_source("dvd"), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["select_source"]) - run_coroutine_threadsafe(ump.async_clear_playlist(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_clear_playlist(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["clear_playlist"]) - run_coroutine_threadsafe(ump.async_set_shuffle(True), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_set_shuffle(True), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["shuffle_set"]) def test_service_call_to_command(self): @@ -707,12 +725,12 @@ def test_service_call_to_command(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() assert 1 == len(service) diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index 32d73c70d4569e..b3dcddfba6a290 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -1,9 +1,9 @@ """The tests for the uptime sensor platform.""" +import asyncio import unittest from unittest.mock import patch from datetime import timedelta -from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.setup import setup_component from homeassistant.components.uptime.sensor import UptimeSensor from tests.common import get_test_home_assistant @@ -46,11 +46,15 @@ def test_uptime_sensor_days_output(self): assert sensor.unit_of_measurement == "days" new_time = sensor.initial + timedelta(days=1) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 1.00 new_time = sensor.initial + timedelta(days=111.499) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 111.50 def test_uptime_sensor_hours_output(self): @@ -59,11 +63,15 @@ def test_uptime_sensor_hours_output(self): assert sensor.unit_of_measurement == "hours" new_time = sensor.initial + timedelta(hours=16) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 16.00 new_time = sensor.initial + timedelta(hours=72.499) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 72.50 def test_uptime_sensor_minutes_output(self): @@ -72,9 +80,13 @@ def test_uptime_sensor_minutes_output(self): assert sensor.unit_of_measurement == "minutes" new_time = sensor.initial + timedelta(minutes=16) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 16.00 new_time = sensor.initial + timedelta(minutes=12.499) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 12.50 diff --git a/tests/test_core.py b/tests/test_core.py index e81ce7a4a5a2d8..5ac13027f288a7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -15,7 +15,6 @@ import homeassistant.core as ha from homeassistant.exceptions import InvalidEntityFormatError, InvalidStateError -from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.const import ( @@ -191,7 +190,7 @@ def test_coro(): for _ in range(3): self.hass.add_job(test_coro()) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( asyncio.wait(self.hass._pending_tasks), loop=self.hass.loop ).result() @@ -216,7 +215,9 @@ def wait_finish_callback(): yield from asyncio.sleep(0) yield from asyncio.sleep(0) - run_coroutine_threadsafe(wait_finish_callback(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + wait_finish_callback(), self.hass.loop + ).result() assert len(self.hass._pending_tasks) == 2 self.hass.block_till_done() @@ -239,7 +240,9 @@ def wait_finish_callback(): for _ in range(2): self.hass.add_job(test_executor) - run_coroutine_threadsafe(wait_finish_callback(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + wait_finish_callback(), self.hass.loop + ).result() assert len(self.hass._pending_tasks) == 2 self.hass.block_till_done() @@ -263,7 +266,9 @@ def wait_finish_callback(): for _ in range(2): self.hass.add_job(test_callback) - run_coroutine_threadsafe(wait_finish_callback(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + wait_finish_callback(), self.hass.loop + ).result() self.hass.block_till_done() diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 023c42cc81781f..8dede61869c39b 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -9,43 +9,6 @@ from homeassistant.util import async_ as hasync -@patch("asyncio.coroutines.iscoroutine") -@patch("concurrent.futures.Future") -@patch("threading.get_ident") -def test_run_coroutine_threadsafe_from_inside_event_loop( - mock_ident, _, mock_iscoroutine -): - """Testing calling run_coroutine_threadsafe from inside an event loop.""" - coro = MagicMock() - loop = MagicMock() - - loop._thread_ident = None - mock_ident.return_value = 5 - mock_iscoroutine.return_value = True - hasync.run_coroutine_threadsafe(coro, loop) - assert len(loop.call_soon_threadsafe.mock_calls) == 1 - - loop._thread_ident = 5 - mock_ident.return_value = 5 - mock_iscoroutine.return_value = True - with pytest.raises(RuntimeError): - hasync.run_coroutine_threadsafe(coro, loop) - assert len(loop.call_soon_threadsafe.mock_calls) == 1 - - loop._thread_ident = 1 - mock_ident.return_value = 5 - mock_iscoroutine.return_value = False - with pytest.raises(TypeError): - hasync.run_coroutine_threadsafe(coro, loop) - assert len(loop.call_soon_threadsafe.mock_calls) == 1 - - loop._thread_ident = 1 - mock_ident.return_value = 5 - mock_iscoroutine.return_value = True - hasync.run_coroutine_threadsafe(coro, loop) - assert len(loop.call_soon_threadsafe.mock_calls) == 2 - - @patch("asyncio.coroutines.iscoroutine") @patch("concurrent.futures.Future") @patch("threading.get_ident") @@ -187,49 +150,6 @@ def target_coroutine( finally: future.done() or future.cancel() - def test_run_coroutine_threadsafe(self): - """Test coroutine submission from a thread to an event loop.""" - future = self.loop.run_in_executor(None, self.target_coroutine) - result = self.loop.run_until_complete(future) - self.assertEqual(result, 3) - - def test_run_coroutine_threadsafe_with_exception(self): - """Test coroutine submission from thread to event loop on exception.""" - future = self.loop.run_in_executor(None, self.target_coroutine, True) - with self.assertRaises(RuntimeError) as exc_context: - self.loop.run_until_complete(future) - self.assertIn("Fail!", exc_context.exception.args) - - def test_run_coroutine_threadsafe_with_invalid(self): - """Test coroutine submission from thread to event loop on invalid.""" - callback = lambda: self.target_coroutine(invalid=True) # noqa - future = self.loop.run_in_executor(None, callback) - with self.assertRaises(ValueError) as exc_context: - self.loop.run_until_complete(future) - self.assertIn("Invalid!", exc_context.exception.args) - - def test_run_coroutine_threadsafe_with_timeout(self): - """Test coroutine submission from thread to event loop on timeout.""" - callback = lambda: self.target_coroutine(timeout=0) # noqa - future = self.loop.run_in_executor(None, callback) - with self.assertRaises(asyncio.TimeoutError): - self.loop.run_until_complete(future) - self.run_briefly(self.loop) - # Check that there's no pending task (add has been cancelled) - if sys.version_info[:2] >= (3, 7): - all_tasks = asyncio.all_tasks - else: - all_tasks = asyncio.Task.all_tasks - for task in all_tasks(self.loop): - self.assertTrue(task.done()) - - def test_run_coroutine_threadsafe_task_cancelled(self): - """Test coroutine submission from tread to event loop on cancel.""" - callback = lambda: self.target_coroutine(cancel=True) # noqa - future = self.loop.run_in_executor(None, callback) - with self.assertRaises(asyncio.CancelledError): - self.loop.run_until_complete(future) - def test_run_callback_threadsafe(self): """Test callback submission from a thread to an event loop.""" future = self.loop.run_in_executor(None, self.target_callback) From 571ab5a97839a17c41b857fee1220925ce116590 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 1 Oct 2019 10:20:30 -0500 Subject: [PATCH 235/296] Plex external config flow (#26936) * Plex external auth config flow * Update requirements_all * Test dependency * Bad await, delay variable * Use hass aiohttp session, bump plexauth * Bump requirements * Bump library version again * Use callback view instead of polling * Update tests for callback view * Reduce timeout with callback * Review feedback * F-string * Wrap sync call * Unused * Revert unnecessary async wrap --- homeassistant/components/plex/config_flow.py | 78 ++++++- homeassistant/components/plex/const.py | 10 + homeassistant/components/plex/manifest.json | 3 +- homeassistant/components/plex/server.py | 12 + homeassistant/components/plex/strings.json | 7 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/plex/mock_classes.py | 4 +- tests/components/plex/test_config_flow.py | 221 +++++++++++++------ 10 files changed, 266 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index cf70b7470cdbcc..dd5401950e9a3c 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -2,10 +2,14 @@ import copy import logging +from aiohttp import web_response import plexapi.exceptions +from plexauth import PlexAuth import requests.exceptions import voluptuous as vol +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( @@ -20,6 +24,8 @@ from homeassistant.util.json import load_json from .const import ( # pylint: disable=unused-import + AUTH_CALLBACK_NAME, + AUTH_CALLBACK_PATH, CONF_SERVER, CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, @@ -30,13 +36,15 @@ DOMAIN, PLEX_CONFIG_FILE, PLEX_SERVER_CONFIG, + X_PLEX_DEVICE_NAME, + X_PLEX_VERSION, + X_PLEX_PRODUCT, + X_PLEX_PLATFORM, ) from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer -USER_SCHEMA = vol.Schema( - {vol.Optional(CONF_TOKEN): str, vol.Optional("manual_setup"): bool} -) +USER_SCHEMA = vol.Schema({vol.Optional("manual_setup"): bool}) _LOGGER = logging.getLogger(__package__) @@ -67,6 +75,8 @@ def __init__(self): self.current_login = {} self.discovery_info = {} self.available_servers = None + self.plexauth = None + self.token = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -74,9 +84,8 @@ async def async_step_user(self, user_input=None): if user_input is not None: if user_input.pop("manual_setup", False): return await self.async_step_manual_setup(user_input) - if CONF_TOKEN in user_input: - return await self.async_step_server_validate(user_input) - errors[CONF_TOKEN] = "no_token" + + return await self.async_step_plex_website_auth() return self.async_show_form( step_id="user", data_schema=USER_SCHEMA, errors=errors @@ -225,6 +234,43 @@ async def async_step_import(self, import_config): _LOGGER.debug("Imported Plex configuration") return await self.async_step_server_validate(import_config) + async def async_step_plex_website_auth(self): + """Begin external auth flow on Plex website.""" + self.hass.http.register_view(PlexAuthorizationCallbackView) + payload = { + "X-Plex-Device-Name": X_PLEX_DEVICE_NAME, + "X-Plex-Version": X_PLEX_VERSION, + "X-Plex-Product": X_PLEX_PRODUCT, + "X-Plex-Device": self.hass.config.location_name, + "X-Plex-Platform": X_PLEX_PLATFORM, + "X-Plex-Model": "Plex OAuth", + } + session = async_get_clientsession(self.hass) + self.plexauth = PlexAuth(payload, session) + await self.plexauth.initiate_auth() + forward_url = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}?flow_id={self.flow_id}" + auth_url = self.plexauth.auth_url(forward_url) + return self.async_external_step(step_id="obtain_token", url=auth_url) + + async def async_step_obtain_token(self, user_input=None): + """Obtain token after external auth completed.""" + token = await self.plexauth.token(10) + + if not token: + return self.async_external_step_done(next_step_id="timed_out") + + self.token = token + return self.async_external_step_done(next_step_id="use_external_token") + + async def async_step_timed_out(self, user_input=None): + """Abort flow when time expires.""" + return self.async_abort(reason="token_request_timeout") + + async def async_step_use_external_token(self, user_input=None): + """Continue server validation with external token.""" + server_config = {CONF_TOKEN: self.token} + return await self.async_step_server_validate(server_config) + class PlexOptionsFlowHandler(config_entries.OptionsFlow): """Handle Plex options.""" @@ -263,3 +309,23 @@ async def async_step_plex_mp_settings(self, user_input=None): } ), ) + + +class PlexAuthorizationCallbackView(HomeAssistantView): + """Handle callback from external auth.""" + + url = AUTH_CALLBACK_PATH + name = AUTH_CALLBACK_NAME + requires_auth = False + + async def get(self, request): + """Receive authorization confirmation.""" + hass = request.app["hass"] + await hass.config_entries.flow.async_configure( + flow_id=request.query["flow_id"], user_input=None + ) + + return web_response.Response( + headers={"content-type": "text/html"}, + text="Success! This window can be closed", + ) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 478dd3754e7d8f..0b436c4e208ce4 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -1,4 +1,6 @@ """Constants for the Plex component.""" +from homeassistant.const import __version__ + DOMAIN = "plex" NAME_FORMAT = "Plex {}" @@ -18,3 +20,11 @@ CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" CONF_SHOW_ALL_CONTROLS = "show_all_controls" + +AUTH_CALLBACK_PATH = "/auth/plex/callback" +AUTH_CALLBACK_NAME = "auth:plex:callback" + +X_PLEX_DEVICE_NAME = "Home Assistant" +X_PLEX_PLATFORM = "Home Assistant" +X_PLEX_PRODUCT = "Home Assistant" +X_PLEX_VERSION = __version__ diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 94d990952a684e..137619b27b00b9 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,8 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/plex", "requirements": [ - "plexapi==3.0.6" + "plexapi==3.0.6", + "plexauth==0.0.4" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 09274472915235..d4393d38c97427 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -11,9 +11,21 @@ CONF_SHOW_ALL_CONTROLS, CONF_USE_EPISODE_ART, DEFAULT_VERIFY_SSL, + X_PLEX_DEVICE_NAME, + X_PLEX_PLATFORM, + X_PLEX_PRODUCT, + X_PLEX_VERSION, ) from .errors import NoServersFound, ServerNotSpecified +# Set default headers sent by plexapi +plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME +plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM +plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT +plexapi.X_PLEX_VERSION = X_PLEX_VERSION +plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers() +plexapi.server.BASE_HEADERS = plexapi.reset_base_headers() + class PlexServer: """Manages a single Plex server connection.""" diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 812e7b81a7cf24..6538d8e887e18c 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -21,9 +21,8 @@ }, "user": { "title": "Connect Plex server", - "description": "Enter a Plex token for automatic setup or manually configure a server.", + "description": "Continue to authorize at plex.tv or manually configure a server.", "data": { - "token": "Plex token", "manual_setup": "Manual setup" } } @@ -31,14 +30,14 @@ "error": { "faulty_credentials": "Authorization failed", "no_servers": "No servers linked to account", - "not_found": "Plex server not found", - "no_token": "Provide a token or select manual setup" + "not_found": "Plex server not found" }, "abort": { "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", "invalid_import": "Imported configuration is invalid", + "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" } }, diff --git a/requirements_all.txt b/requirements_all.txt index ef1b56222b5c9c..ee439d455923b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -964,6 +964,9 @@ pizzapi==0.0.3 # homeassistant.components.plex plexapi==3.0.6 +# homeassistant.components.plex +plexauth==0.0.4 + # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7e4ed37e0012f..949da3ad4029bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -261,6 +261,9 @@ pillow==6.1.0 # homeassistant.components.plex plexapi==3.0.6 +# homeassistant.components.plex +plexauth==0.0.4 + # homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9991a6bc1f0b60..e35a83bd24d7ea 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -113,6 +113,7 @@ "pilight", "pillow", "plexapi", + "plexauth", "pmsensor", "prometheus_client", "ptvsd", diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index d027087828073c..87fb6df597182f 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -1,9 +1,9 @@ """Mock classes used in tests.""" MOCK_HOST_1 = "1.2.3.4" -MOCK_PORT_1 = "32400" +MOCK_PORT_1 = 32400 MOCK_HOST_2 = "4.3.2.1" -MOCK_PORT_2 = "32400" +MOCK_PORT_2 = 32400 class MockAvailableServer: # pylint: disable=too-few-public-methods diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 37cf0fa200cf94..753d565a82b981 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for Plex config flow.""" from unittest.mock import MagicMock, Mock, patch, PropertyMock + +import asynctest import plexapi.exceptions import requests.exceptions @@ -12,6 +14,7 @@ CONF_TOKEN, CONF_URL, ) +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -49,19 +52,28 @@ async def test_bad_credentials(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": True} + ) + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + with patch( - "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized + "plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + user_input={ + CONF_HOST: MOCK_HOST_1, + CONF_PORT: MOCK_PORT_1, + CONF_SSL: False, + CONF_VERIFY_SSL: False, + CONF_TOKEN: "BAD TOKEN", + }, ) - assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"]["base"] == "faulty_credentials" @@ -92,7 +104,6 @@ async def test_import_file_from_discovery(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "create_entry" assert result["title"] == MOCK_NAME_1 assert result["data"][config_flow.CONF_SERVER] == MOCK_NAME_1 @@ -112,7 +123,6 @@ async def test_discovery(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -129,7 +139,6 @@ async def test_discovery_while_in_progress(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -191,7 +200,6 @@ async def test_import_bad_hostname(hass): CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}", }, ) - assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"]["base"] == "not_found" @@ -203,15 +211,25 @@ async def test_unknown_exception(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" - with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception): - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": "user"}, - data={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": True} + ) + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + + with patch("plexapi.server.PlexServer", side_effect=Exception): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: MOCK_HOST_1, + CONF_PORT: MOCK_PORT_1, + CONF_SSL: True, + CONF_VERIFY_SSL: True, + CONF_TOKEN: MOCK_TOKEN, + }, ) assert result["type"] == "abort" @@ -221,23 +239,32 @@ async def test_unknown_exception(hass): async def test_no_servers_found(hass): """Test when no servers are on an account.""" + await async_setup_component(hass, "http", {"http": {}}) + result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" mm_plex_account = MagicMock() mm_plex_account.resources = Mock(return_value=[]) - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): + with patch( + "plexapi.myplex.MyPlexAccount", return_value=mm_plex_account + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"]["base"] == "no_servers" @@ -246,10 +273,11 @@ async def test_no_servers_found(hass): async def test_single_available_server(hass): """Test creating an entry with one server available.""" + await async_setup_component(hass, "http", {"http": {}}) + result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -261,7 +289,11 @@ async def test_single_available_server(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer" - ) as mock_plex_server: + ) as mock_plex_server, asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): type(mock_plex_server.return_value).machineIdentifier = PropertyMock( return_value=MOCK_SERVER_1.clientIdentifier ) @@ -273,10 +305,14 @@ async def test_single_available_server(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" assert result["title"] == MOCK_SERVER_1.name assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name @@ -294,10 +330,11 @@ async def test_single_available_server(hass): async def test_multiple_servers_with_selection(hass): """Test creating an entry with multiple servers available.""" + await async_setup_component(hass, "http", {"http": {}}) + result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -308,7 +345,11 @@ async def test_multiple_servers_with_selection(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer" - ) as mock_plex_server: + ) as mock_plex_server, asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): type(mock_plex_server.return_value).machineIdentifier = PropertyMock( return_value=MOCK_SERVER_1.clientIdentifier ) @@ -320,17 +361,20 @@ async def test_multiple_servers_with_selection(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" assert result["step_id"] == "select_server" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={config_flow.CONF_SERVER: MOCK_SERVER_1.name} ) - assert result["type"] == "create_entry" assert result["title"] == MOCK_SERVER_1.name assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name @@ -348,6 +392,8 @@ async def test_multiple_servers_with_selection(hass): async def test_adding_last_unconfigured_server(hass): """Test automatically adding last unconfigured server when multiple servers on account.""" + await async_setup_component(hass, "http", {"http": {}}) + MockConfigEntry( domain=config_flow.DOMAIN, data={ @@ -359,7 +405,6 @@ async def test_adding_last_unconfigured_server(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -370,7 +415,11 @@ async def test_adding_last_unconfigured_server(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer" - ) as mock_plex_server: + ) as mock_plex_server, asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): type(mock_plex_server.return_value).machineIdentifier = PropertyMock( return_value=MOCK_SERVER_1.clientIdentifier ) @@ -382,10 +431,14 @@ async def test_adding_last_unconfigured_server(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" assert result["title"] == MOCK_SERVER_1.name assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name @@ -414,7 +467,9 @@ async def test_already_configured(hass): mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.server.PlexServer") as mock_plex_server: + with patch("plexapi.server.PlexServer") as mock_plex_server, asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN): type(mock_plex_server.return_value).machineIdentifier = PropertyMock( return_value=MOCK_SERVER_1.clientIdentifier ) @@ -424,10 +479,10 @@ async def test_already_configured(hass): type( # pylint: disable=protected-access mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + result = await flow.async_step_import( {CONF_TOKEN: MOCK_TOKEN, CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}"} ) - assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -435,6 +490,8 @@ async def test_already_configured(hass): async def test_all_available_servers_configured(hass): """Test when all available servers are already configured.""" + await async_setup_component(hass, "http", {"http": {}}) + MockConfigEntry( domain=config_flow.DOMAIN, data={ @@ -454,7 +511,6 @@ async def test_all_available_servers_configured(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -463,13 +519,21 @@ async def test_all_available_servers_configured(hass): mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): + with patch( + "plexapi.myplex.MyPlexAccount", return_value=mm_plex_account + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "abort" assert result["reason"] == "all_configured" @@ -480,14 +544,12 @@ async def test_manual_config(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: "", "manual_setup": True} + result["flow_id"], user_input={"manual_setup": True} ) - assert result["type"] == "form" assert result["step_id"] == "manual_setup" @@ -508,13 +570,12 @@ async def test_manual_config(hass): result["flow_id"], user_input={ CONF_HOST: MOCK_HOST_1, - CONF_PORT: int(MOCK_PORT_1), + CONF_PORT: MOCK_PORT_1, CONF_SSL: True, CONF_VERIFY_SSL: True, CONF_TOKEN: MOCK_TOKEN, }, ) - assert result["type"] == "create_entry" assert result["title"] == MOCK_SERVER_1.name assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name @@ -529,25 +590,6 @@ async def test_manual_config(hass): assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_no_token(hass): - """Test failing when no token provided.""" - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - - assert result["type"] == "form" - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) - - assert result["type"] == "form" - assert result["step_id"] == "user" - assert result["errors"][CONF_TOKEN] == "no_token" - - async def test_option_flow(hass): """Test config flow selection of one of two bridges.""" @@ -557,7 +599,6 @@ async def test_option_flow(hass): result = await hass.config_entries.options.flow.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == "form" assert result["step_id"] == "plex_mp_settings" @@ -575,3 +616,57 @@ async def test_option_flow(hass): config_flow.CONF_SHOW_ALL_CONTROLS: True, } } + + +async def test_external_timed_out(hass): + """Test when external flow times out.""" + + await async_setup_component(hass, "http", {"http": {}}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=None + ): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": False} + ) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "abort" + assert result["reason"] == "token_request_timeout" + + +async def test_callback_view(hass, aiohttp_client): + """Test callback view.""" + + await async_setup_component(hass, "http", {"http": {}}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": False} + ) + assert result["type"] == "external" + + client = await aiohttp_client(hass.http.app) + forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' + + resp = await client.get(forward_url) + assert resp.status == 200 From 3b0744d0214d8cdbf19ecfa95b4a964bc7ae7e34 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Oct 2019 21:19:50 +0200 Subject: [PATCH 236/296] Bump attrs to 19.2.0 (#27102) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 47325a6c930e61..684285a1cf10b3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiohttp==3.6.1 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 -attrs==19.1.0 +attrs==19.2.0 bcrypt==3.1.7 certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" diff --git a/requirements_all.txt b/requirements_all.txt index ee439d455923b5..4c963ddab23175 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ aiohttp==3.6.1 astral==1.10.1 async_timeout==3.0.1 -attrs==19.1.0 +attrs==19.2.0 bcrypt==3.1.7 certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" diff --git a/setup.py b/setup.py index 26f112bb008207..d842ae39ae1b77 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ "aiohttp==3.6.1", "astral==1.10.1", "async_timeout==3.0.1", - "attrs==19.1.0", + "attrs==19.2.0", "bcrypt==3.1.7", "certifi>=2019.6.16", 'contextvars==2.4;python_version<"3.7"', From ca9b3b5a4c662c89e5c834bdc4f205d1a860e951 Mon Sep 17 00:00:00 2001 From: rolfberkenbosch <30292281+rolfberkenbosch@users.noreply.github.com> Date: Tue, 1 Oct 2019 21:20:28 +0200 Subject: [PATCH 237/296] Update meteoalertapi to version 0.1.6 (#27099) --- homeassistant/components/meteoalarm/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 2148375621307c..692e55260850d3 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -3,7 +3,7 @@ "name": "meteoalarm", "documentation": "https://www.home-assistant.io/components/meteoalarm", "requirements": [ - "meteoalertapi==0.1.5" + "meteoalertapi==0.1.6" ], "dependencies": [], "codeowners": ["@rolfberkenbosch"] diff --git a/requirements_all.txt b/requirements_all.txt index 4c963ddab23175..dd38be3bfbd638 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -805,7 +805,7 @@ mbddns==0.1.2 messagebird==1.2.0 # homeassistant.components.meteoalarm -meteoalertapi==0.1.5 +meteoalertapi==0.1.6 # homeassistant.components.meteo_france meteofrance==0.3.7 From 2e4c92104d5b7b6f354377df8ae3afc51ad7ca66 Mon Sep 17 00:00:00 2001 From: chriscla Date: Tue, 1 Oct 2019 12:51:11 -0700 Subject: [PATCH 238/296] Nzbget services (#26900) * Add NZBGet Queue control. * Fix error message * Addressing review comments * Moving import to top of file. --- homeassistant/components/nzbget/__init__.py | 64 ++++++++++++++++++- homeassistant/components/nzbget/services.yaml | 14 ++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/nzbget/services.yaml diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 37744dce180342..ae3ce7c594479f 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -20,15 +20,26 @@ _LOGGER = logging.getLogger(__name__) +ATTR_SPEED = "speed" + DOMAIN = "nzbget" DATA_NZBGET = "data_nzbget" DATA_UPDATED = "nzbget_data_updated" DEFAULT_NAME = "NZBGet" DEFAULT_PORT = 6789 +DEFAULT_SPEED_LIMIT = 1000 # 1 Megabyte/Sec DEFAULT_SCAN_INTERVAL = timedelta(seconds=5) +SERVICE_PAUSE = "pause" +SERVICE_RESUME = "resume" +SERVICE_SET_SPEED = "set_speed" + +SPEED_LIMIT_SCHEMA = vol.Schema( + {vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int} +) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -51,6 +62,7 @@ def setup(hass, config): """Set up the NZBGet sensors.""" + host = config[DOMAIN][CONF_HOST] port = config[DOMAIN][CONF_PORT] ssl = "s" if config[DOMAIN][CONF_SSL] else "" @@ -71,6 +83,28 @@ def setup(hass, config): nzbget_data = hass.data[DATA_NZBGET] = NZBGetData(hass, nzbget_api) nzbget_data.update() + def service_handler(service): + """Handle service calls.""" + if service.service == SERVICE_PAUSE: + nzbget_data.pause_download() + elif service.service == SERVICE_RESUME: + nzbget_data.resume_download() + elif service.service == SERVICE_SET_SPEED: + limit = service.data[ATTR_SPEED] + nzbget_data.rate(limit) + + hass.services.register( + DOMAIN, SERVICE_PAUSE, service_handler, schema=vol.Schema({}) + ) + + hass.services.register( + DOMAIN, SERVICE_RESUME, service_handler, schema=vol.Schema({}) + ) + + hass.services.register( + DOMAIN, SERVICE_SET_SPEED, service_handler, schema=SPEED_LIMIT_SCHEMA + ) + def refresh(event_time): """Get the latest data from NZBGet.""" nzbget_data.update() @@ -96,10 +130,36 @@ def __init__(self, hass, api): def update(self): """Get the latest data from NZBGet instance.""" + try: self.status = self._api.status() self.available = True dispatcher_send(self.hass, DATA_UPDATED) - except pynzbgetapi.NZBGetAPIException: + except pynzbgetapi.NZBGetAPIException as err: self.available = False - _LOGGER.error("Unable to refresh NZBGet data") + _LOGGER.error("Unable to refresh NZBGet data: %s", err) + + def pause_download(self): + """Pause download queue.""" + + try: + self._api.pausedownload() + except pynzbgetapi.NZBGetAPIException as err: + _LOGGER.error("Unable to pause queue: %s", err) + + def resume_download(self): + """Resume download queue.""" + + try: + self._api.resumedownload() + except pynzbgetapi.NZBGetAPIException as err: + _LOGGER.error("Unable to resume download queue: %s", err) + + def rate(self, limit): + """Set download speed.""" + + try: + if not self._api.rate(limit): + _LOGGER.error("Limit was out of range") + except pynzbgetapi.NZBGetAPIException as err: + _LOGGER.error("Unable to set download speed: %s", err) diff --git a/homeassistant/components/nzbget/services.yaml b/homeassistant/components/nzbget/services.yaml new file mode 100644 index 00000000000000..84e4af15b5d735 --- /dev/null +++ b/homeassistant/components/nzbget/services.yaml @@ -0,0 +1,14 @@ +# Describes the format for available nzbget services + +pause: + description: Pause download queue. + +resume: + description: Resume download queue. + +set_speed: + description: Set download speed limit + fields: + speed: + description: Speed limit in KB/s. 0 is unlimited. + example: 1000 \ No newline at end of file From 8d251c190fd15b5096a6fce038697b4eb72a31aa Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Tue, 1 Oct 2019 22:02:43 +0200 Subject: [PATCH 239/296] Delete here_travel_time dead code COORDINATE_SCHEMA (#27090) --- homeassistant/components/here_travel_time/sensor.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index ba4908fe85c3ac..8fd4b4fe94a5cb 100755 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -93,13 +93,6 @@ NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" -COORDINATE_SCHEMA = vol.Schema( - { - vol.Inclusive(CONF_DESTINATION_LATITUDE, "coordinates"): cv.latitude, - vol.Inclusive(CONF_DESTINATION_LONGITUDE, "coordinates"): cv.longitude, - } -) - PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_DESTINATION_LATITUDE, CONF_DESTINATION_ENTITY_ID), cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID), From e033c46c91a265a34e2bf9c5ff299c0f294b4742 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 1 Oct 2019 16:26:33 -0500 Subject: [PATCH 240/296] Add missing http dependency (#27097) --- homeassistant/components/plex/manifest.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 137619b27b00b9..8e068c909c5d5e 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -7,7 +7,9 @@ "plexapi==3.0.6", "plexauth==0.0.4" ], - "dependencies": [], + "dependencies": [ + "http" + ], "codeowners": [ "@jjlawren" ] From ee45431d0eeb629afe038c72d686a5cc5e4b0792 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Tue, 1 Oct 2019 17:28:13 -0400 Subject: [PATCH 241/296] Add entity registry support to ecobee integration (#27088) * Add unique id to platforms Add unique id for binary sensor, climate, and sensor. * Add unique id to weather platform * Simplify unique_id for weather platform * Fix lint for unique_id in sensor * Fix lint for unique_id in binary sensor --- homeassistant/components/ecobee/binary_sensor.py | 9 +++++++++ homeassistant/components/ecobee/climate.py | 5 +++++ homeassistant/components/ecobee/sensor.py | 9 +++++++++ homeassistant/components/ecobee/weather.py | 5 +++++ 4 files changed, 28 insertions(+) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 8b7b819cfc7141..68d8a88df47b64 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -43,6 +43,15 @@ def name(self): """Return the name of the Ecobee sensor.""" return self._name.rstrip() + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] == self.sensor_name: + if "code" in sensor: + return f"{sensor['code']}-{self.device_class}" + return f"{sensor['id']}-{self.device_class}" + @property def is_on(self): """Return the status of the sensor.""" diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 6eccdccf8c6ac0..f930282ba7b90a 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -305,6 +305,11 @@ def name(self): """Return the name of the Ecobee Thermostat.""" return self.thermostat["name"] + @property + def unique_id(self): + """Return a unique identifier for this ecobee thermostat.""" + return self.thermostat["identifier"] + @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index e62d68dc9bcfcd..8cf9af0e3b445f 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -54,6 +54,15 @@ def name(self): """Return the name of the Ecobee sensor.""" return self._name + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] == self.sensor_name: + if "code" in sensor: + return f"{sensor['code']}-{self.device_class}" + return f"{sensor['id']}-{self.device_class}" + @property def device_class(self): """Return the device class of the sensor.""" diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index dd3112b636e0c3..6175405638e846 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -61,6 +61,11 @@ def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return a unique identifier for the weather platform.""" + return self.data.ecobee.get_thermostat(self._index)["identifier"] + @property def condition(self): """Return the current condition.""" From 26d78cab60dfd9c495af8ac5eaf13b664aba2700 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Tue, 1 Oct 2019 23:38:48 +0200 Subject: [PATCH 242/296] Update opentherm_gw.climate to match Climate 1.0 (#25931) * Update opentherm_gw.climate to match Climate 1.0 * Change hvac_mode to reflect last active hvac_action --- .../components/opentherm_gw/climate.py | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index d37422d4177a61..fab028560bb66e 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -5,11 +5,14 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_COOL, HVAC_MODE_HEAT, - HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, PRESET_AWAY, + PRESET_NONE, SUPPORT_PRESET_MODE, ) from homeassistant.const import ( @@ -47,8 +50,9 @@ def __init__(self, gw_dev): self.friendly_name = gw_dev.name self.floor_temp = gw_dev.climate_config[CONF_FLOOR_TEMP] self.temp_precision = gw_dev.climate_config.get(CONF_PRECISION) - self._current_operation = HVAC_MODE_OFF + self._current_operation = None self._current_temperature = None + self._hvac_mode = HVAC_MODE_HEAT self._new_target_temperature = None self._target_temperature = None self._away_mode_a = None @@ -70,11 +74,13 @@ def receive_report(self, status): flame_on = status.get(gw_vars.DATA_SLAVE_FLAME_ON) cooling_active = status.get(gw_vars.DATA_SLAVE_COOLING_ACTIVE) if ch_active and flame_on: - self._current_operation = HVAC_MODE_HEAT + self._current_operation = CURRENT_HVAC_HEAT + self._hvac_mode = HVAC_MODE_HEAT elif cooling_active: - self._current_operation = HVAC_MODE_COOL + self._current_operation = CURRENT_HVAC_COOL + self._hvac_mode = HVAC_MODE_COOL else: - self._current_operation = HVAC_MODE_OFF + self._current_operation = CURRENT_HVAC_IDLE self._current_temperature = status.get(gw_vars.DATA_ROOM_TEMP) temp_upd = status.get(gw_vars.DATA_ROOM_SETPOINT) @@ -138,16 +144,25 @@ def temperature_unit(self): """Return the unit of measurement used by the platform.""" return TEMP_CELSIUS + @property + def hvac_action(self): + """Return current HVAC operation.""" + return self._current_operation + @property def hvac_mode(self): """Return current HVAC mode.""" - return self._current_operation + return self._hvac_mode @property def hvac_modes(self): """Return available HVAC modes.""" return [] + def set_hvac_mode(self, hvac_mode): + """Set the HVAC mode.""" + _LOGGER.warning("Changing HVAC mode is not supported") + @property def current_temperature(self): """Return the current temperature.""" @@ -176,6 +191,7 @@ def preset_mode(self): """Return current preset mode.""" if self._away_state_a or self._away_state_b: return PRESET_AWAY + return PRESET_NONE @property def preset_modes(self): From 2090d650c645fb5d3d3c6c8c0f3037a7f9350fe9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 2 Oct 2019 00:32:11 +0000 Subject: [PATCH 243/296] [ci skip] Translation update --- .../components/binary_sensor/.translations/ko.json | 8 ++++++-- homeassistant/components/plex/.translations/ca.json | 1 + homeassistant/components/plex/.translations/en.json | 3 ++- homeassistant/components/plex/.translations/ru.json | 1 + homeassistant/components/soma/.translations/ko.json | 13 +++++++++++++ homeassistant/components/soma/.translations/no.json | 13 +++++++++++++ .../components/tellduslive/.translations/no.json | 1 + homeassistant/components/zha/.translations/no.json | 2 +- 8 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/soma/.translations/ko.json create mode 100644 homeassistant/components/soma/.translations/no.json diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json index 02443d449c543f..3c12eabe8ff0c8 100644 --- a/homeassistant/components/binary_sensor/.translations/ko.json +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", + "is_bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc2b5\ub2c8\ub2e4", "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", @@ -18,7 +18,7 @@ "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", + "is_not_bat_low": "{entity_name} \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc2b5\ub2c8\ub2e4", "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", @@ -43,6 +43,10 @@ "is_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", "is_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4" + }, + "trigger_type": { + "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871", + "closed": "{entity_name} \ub2eb\ud798" } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 14607868907960..11e11ebc6fe85b 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -5,6 +5,7 @@ "already_configured": "Aquest servidor Plex ja est\u00e0 configurat", "already_in_progress": "S\u2019est\u00e0 configurant Plex", "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", + "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del testimoni.", "unknown": "Ha fallat per motiu desconegut" }, "error": { diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index d3c4c0d5a7825b..efdd75b14819ab 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -5,6 +5,7 @@ "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", "invalid_import": "Imported configuration is invalid", + "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" }, "error": { @@ -36,7 +37,7 @@ "manual_setup": "Manual setup", "token": "Plex token" }, - "description": "Enter a Plex token for automatic setup or manually configure a server.", + "description": "Continue to authorize at plex.tv or manually configure a server.", "title": "Connect Plex server" } }, diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index c547e4306b4ad5..2b63840d00198b 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -5,6 +5,7 @@ "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", + "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430", "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435" }, "error": { diff --git a/homeassistant/components/soma/.translations/ko.json b/homeassistant/components/soma/.translations/ko.json new file mode 100644 index 00000000000000..53146bebf83c16 --- /dev/null +++ b/homeassistant/components/soma/.translations/ko.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 Soma \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "Soma \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + }, + "create_entry": { + "default": "Soma \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json new file mode 100644 index 00000000000000..1ea53b778ea11a --- /dev/null +++ b/homeassistant/components/soma/.translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bare konfigurere en Soma-konto.", + "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + }, + "create_entry": { + "default": "Vellykket autentisering med Somfy." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/no.json b/homeassistant/components/tellduslive/.translations/no.json index 090de51703654f..3258cf2ddcad3a 100644 --- a/homeassistant/components/tellduslive/.translations/no.json +++ b/homeassistant/components/tellduslive/.translations/no.json @@ -18,6 +18,7 @@ "data": { "host": "Vert" }, + "description": "Tom", "title": "Velg endepunkt." } }, diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 95550ca0999966..390069b7698c4e 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -53,7 +53,7 @@ "device_rotated": "Enheten roterte \"{subtype}\"", "device_shaken": "Enhet er ristet", "device_slid": "Enheten skled \"{subtype}\"", - "device_tilted": "Enhet vippet", + "device_tilted": "Enhetn skr\u00e5stilt", "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", From 5a1da72d5ed26d2409d74caddee5610a1f71f700 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Oct 2019 05:35:36 +0200 Subject: [PATCH 244/296] Improve validation of device action config (#27029) --- homeassistant/components/automation/config.py | 10 ++++-- homeassistant/helpers/script.py | 29 ++++++++++----- .../components/device_automation/test_init.py | 36 +++++++++++++++++++ 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 04b764e271ca48..3f48e2afde67f8 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -7,10 +7,10 @@ from homeassistant.const import CONF_PLATFORM from homeassistant.config import async_log_exception, config_without_domain from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform +from homeassistant.helpers import config_per_platform, script from homeassistant.loader import IntegrationNotFound -from . import CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA +from . import CONF_ACTION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -32,6 +32,12 @@ async def async_validate_config_item(hass, config, full_config=None): ) triggers.append(trigger) config[CONF_TRIGGER] = triggers + + actions = [] + for action in config[CONF_ACTION]: + action = await script.async_validate_action_config(hass, action) + actions.append(action) + config[CONF_ACTION] = actions except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: async_log_exception(ex, DOMAIN, full_config or config, hass) return None diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index a4f6afa16366af..e383f1013ab204 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -8,13 +8,9 @@ import voluptuous as vol +import homeassistant.components.device_automation as device_automation from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE -from homeassistant.const import ( - CONF_CONDITION, - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_TIMEOUT, -) +from homeassistant.const import CONF_CONDITION, CONF_DEVICE_ID, CONF_TIMEOUT from homeassistant import exceptions from homeassistant.helpers import ( service, @@ -27,7 +23,6 @@ async_track_template, ) from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import async_get_integration import homeassistant.util.dt as date_util from homeassistant.util.async_ import run_callback_threadsafe @@ -86,6 +81,21 @@ def call_from_config( Script(hass, cv.SCRIPT_SCHEMA(config)).run(variables, context) +async def async_validate_action_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + action_type = _determine_action(config) + + if action_type == ACTION_DEVICE_AUTOMATION: + platform = await device_automation.async_get_device_automation_platform( + hass, config, "action" + ) + config = platform.ACTION_SCHEMA(config) + + return config + + class _StopScript(Exception): """Throw if script needs to stop.""" @@ -335,8 +345,9 @@ async def _async_device_automation(self, action, variables, context): """ self.last_action = action.get(CONF_ALIAS, "device automation") self._log("Executing step %s" % self.last_action) - integration = await async_get_integration(self.hass, action[CONF_DOMAIN]) - platform = integration.get_platform("device_action") + platform = await device_automation.async_get_device_automation_platform( + self.hass, action, "action" + ) await platform.async_call_action_from_config( self.hass, action, variables, context ) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 6dcd8391bf8954..acfa853d596f27 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -185,6 +185,25 @@ async def test_automation_with_non_existing_integration(hass, caplog): assert "Integration 'beer' not found" in caplog.text +async def test_automation_with_integration_without_device_action(hass, caplog): + """Test automation with integration without device action support.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": {"device_id": "", "domain": "test"}, + } + }, + ) + + assert ( + "Integration 'test' does not support device automation actions" in caplog.text + ) + + async def test_automation_with_integration_without_device_trigger(hass, caplog): """Test automation with integration without device trigger support.""" assert await async_setup_component( @@ -208,6 +227,23 @@ async def test_automation_with_integration_without_device_trigger(hass, caplog): ) +async def test_automation_with_bad_action(hass, caplog): + """Test automation with bad device action.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": {"device_id": "", "domain": "light"}, + } + }, + ) + + assert "required key not provided" in caplog.text + + async def test_automation_with_bad_trigger(hass, caplog): """Test automation with bad device trigger.""" assert await async_setup_component( From 7d2a8b81370fdf643ce059e8f53d831de442eeaa Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Tue, 1 Oct 2019 23:17:30 -0700 Subject: [PATCH 245/296] Bump adb-shell to 0.0.3 (#27108) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index c085addad4dd37..dbd76a2bc9b0bd 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/components/androidtv", "requirements": [ - "adb-shell==0.0.2", + "adb-shell==0.0.3", "androidtv==0.0.28" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index dd38be3bfbd638..385f2d0e1a2495 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.2 +adb-shell==0.0.3 # homeassistant.components.adguard adguardhome==0.2.1 From ce2e80339c67f3a9ce6bb230f8345f13da660236 Mon Sep 17 00:00:00 2001 From: Chris Colohan Date: Wed, 2 Oct 2019 00:50:45 -0700 Subject: [PATCH 246/296] Add Vera last user and low battery attributes (#27043) * Add in attributes to track when a user unlocks the lock with a PIN, and when the battery runs low. * Vera attributes for who entered PIN on lock, and low battery warning. * Changed last_user_id to use changed_by interface. * Bump pyvera version to 0.3.6; remove guard code for earlier pyvera versions. * Bump pyvera version to 0.3.6 --- homeassistant/components/vera/lock.py | 32 +++++++++++++++++++++ homeassistant/components/vera/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index cf2d6d25c4a340..23b62bb0331466 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -8,6 +8,9 @@ _LOGGER = logging.getLogger(__name__) +ATTR_LAST_USER_NAME = "changed_by_name" +ATTR_LOW_BATTERY = "low_battery" + def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return Vera locks.""" @@ -44,6 +47,35 @@ def is_locked(self): """Return true if device is on.""" return self._state == STATE_LOCKED + @property + def device_state_attributes(self): + """Who unlocked the lock and did a low battery alert fire. + + Reports on the previous poll cycle. + changed_by_name is a string like 'Bob'. + low_battery is 1 if an alert fired, 0 otherwise. + """ + data = super().device_state_attributes + + last_user = self.vera_device.get_last_user_alert() + if last_user is not None: + data[ATTR_LAST_USER_NAME] = last_user[1] + + data[ATTR_LOW_BATTERY] = self.vera_device.get_low_battery_alert() + return data + + @property + def changed_by(self): + """Who unlocked the lock. + + Reports on the previous poll cycle. + changed_by is an integer user ID. + """ + last_user = self.vera_device.get_last_user_alert() + if last_user is not None: + return last_user[0] + return None + def update(self): """Update state by the Vera device callback.""" self._state = ( diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 8365ca1a765218..b2f1581e76f191 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -3,7 +3,7 @@ "name": "Vera", "documentation": "https://www.home-assistant.io/components/vera", "requirements": [ - "pyvera==0.3.4" + "pyvera==0.3.6" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 385f2d0e1a2495..245f91dc9c65be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1625,7 +1625,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.4 +pyvera==0.3.6 # homeassistant.components.vesync pyvesync==1.1.0 From 8488b572150a5b3f667efc50848c9ecbd40fe4f8 Mon Sep 17 00:00:00 2001 From: Brendon Baumgartner Date: Wed, 2 Oct 2019 01:25:35 -0700 Subject: [PATCH 247/296] Add neural support to amazon polly (#27101) * amazon polly - add neural support * bumped boto3 for route53 to 1.9.233 --- homeassistant/components/amazon_polly/manifest.json | 2 +- homeassistant/components/amazon_polly/tts.py | 10 ++++++++-- homeassistant/components/route53/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index 19140aac939682..20d443f225eb18 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -3,7 +3,7 @@ "name": "Amazon polly", "documentation": "https://www.home-assistant.io/components/amazon_polly", "requirements": [ - "boto3==1.9.16" + "boto3==1.9.233" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index c7098867ee899e..64b8b71457cd3f 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -33,6 +33,7 @@ "sa-east-1", ] +CONF_ENGINE = "engine" CONF_VOICE = "voice" CONF_OUTPUT_FORMAT = "output_format" CONF_SAMPLE_RATE = "sample_rate" @@ -101,10 +102,12 @@ SUPPORTED_OUTPUT_FORMATS = ["mp3", "ogg_vorbis", "pcm"] -SUPPORTED_SAMPLE_RATES = ["8000", "16000", "22050"] +SUPPORTED_ENGINES = ["neural", "standard"] + +SUPPORTED_SAMPLE_RATES = ["8000", "16000", "22050", "24000"] SUPPORTED_SAMPLE_RATES_MAP = { - "mp3": ["8000", "16000", "22050"], + "mp3": ["8000", "16000", "22050", "24000"], "ogg_vorbis": ["8000", "16000", "22050"], "pcm": ["8000", "16000"], } @@ -113,6 +116,7 @@ CONTENT_TYPE_EXTENSIONS = {"audio/mpeg": "mp3", "audio/ogg": "ogg", "audio/pcm": "pcm"} +DEFAULT_ENGINE = "standard" DEFAULT_VOICE = "Joanna" DEFAULT_OUTPUT_FORMAT = "mp3" DEFAULT_TEXT_TYPE = "text" @@ -126,6 +130,7 @@ vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string, vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string, vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES), + vol.Optional(CONF_ENGINE, default=DEFAULT_ENGINE): vol.In(SUPPORTED_ENGINES), vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT): vol.In( SUPPORTED_OUTPUT_FORMATS ), @@ -225,6 +230,7 @@ def get_tts_audio(self, message, language=None, options=None): return None, None resp = self.client.synthesize_speech( + Engine=self.config[CONF_ENGINE], OutputFormat=self.config[CONF_OUTPUT_FORMAT], SampleRate=self.config[CONF_SAMPLE_RATE], Text=message, diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index d377ca7dca03c0..7ac91964a98720 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -3,7 +3,7 @@ "name": "Route53", "documentation": "https://www.home-assistant.io/components/route53", "requirements": [ - "boto3==1.9.16", + "boto3==1.9.233", "ipify==1.0.0" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 245f91dc9c65be..35ee05977d8e22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -302,7 +302,7 @@ bomradarloop==0.1.3 # homeassistant.components.amazon_polly # homeassistant.components.route53 -boto3==1.9.16 +boto3==1.9.233 # homeassistant.components.braviatv braviarc-homeassistant==0.3.7.dev0 From ed49b2f155e929e8f9288d005f53d1d8d396567b Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 2 Oct 2019 08:38:14 -0700 Subject: [PATCH 248/296] Bump androidtv to 0.0.29 (#27120) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index dbd76a2bc9b0bd..dc2d31c23583ef 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/components/androidtv", "requirements": [ "adb-shell==0.0.3", - "androidtv==0.0.28" + "androidtv==0.0.29" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index 35ee05977d8e22..2cf2c765e8b0e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -200,7 +200,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.28 +androidtv==0.0.29 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 949da3ad4029bb..bb29540d6d93c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -80,7 +80,7 @@ aiowwlln==2.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.28 +androidtv==0.0.29 # homeassistant.components.apns apns2==0.3.0 From c7da781efcb47c29e1724d1716d729037c5b38bc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Oct 2019 18:25:44 +0200 Subject: [PATCH 249/296] Update documentation link URL for integrations in all manifests (#27114) --- homeassistant/components/abode/manifest.json | 2 +- homeassistant/components/acer_projector/manifest.json | 2 +- homeassistant/components/actiontec/manifest.json | 2 +- homeassistant/components/adguard/manifest.json | 2 +- homeassistant/components/ads/manifest.json | 2 +- homeassistant/components/aftership/manifest.json | 2 +- homeassistant/components/air_quality/manifest.json | 2 +- homeassistant/components/airvisual/manifest.json | 2 +- homeassistant/components/aladdin_connect/manifest.json | 2 +- homeassistant/components/alarm_control_panel/manifest.json | 2 +- homeassistant/components/alarmdecoder/manifest.json | 2 +- homeassistant/components/alarmdotcom/manifest.json | 2 +- homeassistant/components/alert/manifest.json | 2 +- homeassistant/components/alexa/manifest.json | 2 +- homeassistant/components/alpha_vantage/manifest.json | 2 +- homeassistant/components/amazon_polly/manifest.json | 2 +- homeassistant/components/ambiclimate/manifest.json | 2 +- homeassistant/components/ambient_station/manifest.json | 2 +- homeassistant/components/amcrest/manifest.json | 2 +- homeassistant/components/ampio/manifest.json | 2 +- homeassistant/components/android_ip_webcam/manifest.json | 2 +- homeassistant/components/androidtv/manifest.json | 2 +- homeassistant/components/anel_pwrctrl/manifest.json | 2 +- homeassistant/components/anthemav/manifest.json | 2 +- homeassistant/components/apache_kafka/manifest.json | 2 +- homeassistant/components/apcupsd/manifest.json | 2 +- homeassistant/components/api/manifest.json | 2 +- homeassistant/components/apns/manifest.json | 2 +- homeassistant/components/apple_tv/manifest.json | 2 +- homeassistant/components/aprs/manifest.json | 2 +- homeassistant/components/aqualogic/manifest.json | 2 +- homeassistant/components/aquostv/manifest.json | 2 +- homeassistant/components/arcam_fmj/manifest.json | 2 +- homeassistant/components/arduino/manifest.json | 2 +- homeassistant/components/arest/manifest.json | 2 +- homeassistant/components/arlo/manifest.json | 2 +- homeassistant/components/aruba/manifest.json | 2 +- homeassistant/components/arwn/manifest.json | 2 +- homeassistant/components/asterisk_cdr/manifest.json | 2 +- homeassistant/components/asterisk_mbox/manifest.json | 2 +- homeassistant/components/asuswrt/manifest.json | 2 +- homeassistant/components/atome/manifest.json | 2 +- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/aurora/manifest.json | 2 +- homeassistant/components/aurora_abb_powerone/manifest.json | 2 +- homeassistant/components/auth/manifest.json | 2 +- homeassistant/components/automatic/manifest.json | 2 +- homeassistant/components/automation/manifest.json | 2 +- homeassistant/components/avea/manifest.json | 2 +- homeassistant/components/avion/manifest.json | 2 +- homeassistant/components/awair/manifest.json | 2 +- homeassistant/components/aws/manifest.json | 2 +- homeassistant/components/axis/manifest.json | 2 +- homeassistant/components/azure_event_hub/manifest.json | 2 +- homeassistant/components/baidu/manifest.json | 2 +- homeassistant/components/bayesian/manifest.json | 2 +- homeassistant/components/bbb_gpio/manifest.json | 2 +- homeassistant/components/bbox/manifest.json | 2 +- homeassistant/components/beewi_smartclim/manifest.json | 2 +- homeassistant/components/bh1750/manifest.json | 2 +- homeassistant/components/binary_sensor/manifest.json | 2 +- homeassistant/components/bitcoin/manifest.json | 2 +- homeassistant/components/bizkaibus/manifest.json | 2 +- homeassistant/components/blackbird/manifest.json | 2 +- homeassistant/components/blink/manifest.json | 2 +- homeassistant/components/blinksticklight/manifest.json | 2 +- homeassistant/components/blinkt/manifest.json | 2 +- homeassistant/components/blockchain/manifest.json | 2 +- homeassistant/components/bloomsky/manifest.json | 2 +- homeassistant/components/bluesound/manifest.json | 2 +- homeassistant/components/bluetooth_le_tracker/manifest.json | 2 +- homeassistant/components/bluetooth_tracker/manifest.json | 2 +- homeassistant/components/bme280/manifest.json | 2 +- homeassistant/components/bme680/manifest.json | 2 +- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- homeassistant/components/bom/manifest.json | 2 +- homeassistant/components/braviatv/manifest.json | 2 +- homeassistant/components/broadlink/manifest.json | 2 +- homeassistant/components/brottsplatskartan/manifest.json | 2 +- homeassistant/components/browser/manifest.json | 2 +- homeassistant/components/brunt/manifest.json | 2 +- homeassistant/components/bt_home_hub_5/manifest.json | 2 +- homeassistant/components/bt_smarthub/manifest.json | 2 +- homeassistant/components/buienradar/manifest.json | 2 +- homeassistant/components/caldav/manifest.json | 2 +- homeassistant/components/calendar/manifest.json | 2 +- homeassistant/components/camera/manifest.json | 2 +- homeassistant/components/canary/manifest.json | 2 +- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cert_expiry/manifest.json | 2 +- homeassistant/components/channels/manifest.json | 2 +- homeassistant/components/cisco_ios/manifest.json | 2 +- homeassistant/components/cisco_mobility_express/manifest.json | 2 +- homeassistant/components/cisco_webex_teams/manifest.json | 2 +- homeassistant/components/ciscospark/manifest.json | 2 +- homeassistant/components/citybikes/manifest.json | 2 +- homeassistant/components/clementine/manifest.json | 2 +- homeassistant/components/clickatell/manifest.json | 2 +- homeassistant/components/clicksend/manifest.json | 2 +- homeassistant/components/clicksend_tts/manifest.json | 2 +- homeassistant/components/climate/manifest.json | 2 +- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/components/cloudflare/manifest.json | 2 +- homeassistant/components/cmus/manifest.json | 2 +- homeassistant/components/co2signal/manifest.json | 2 +- homeassistant/components/coinbase/manifest.json | 2 +- homeassistant/components/coinmarketcap/manifest.json | 2 +- homeassistant/components/comed_hourly_pricing/manifest.json | 2 +- homeassistant/components/comfoconnect/manifest.json | 2 +- homeassistant/components/command_line/manifest.json | 2 +- homeassistant/components/concord232/manifest.json | 2 +- homeassistant/components/config/manifest.json | 2 +- homeassistant/components/configurator/manifest.json | 2 +- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/components/coolmaster/manifest.json | 2 +- homeassistant/components/counter/manifest.json | 2 +- homeassistant/components/cover/manifest.json | 2 +- homeassistant/components/cppm_tracker/manifest.json | 2 +- homeassistant/components/cpuspeed/manifest.json | 2 +- homeassistant/components/crimereports/manifest.json | 2 +- homeassistant/components/cups/manifest.json | 2 +- homeassistant/components/currencylayer/manifest.json | 2 +- homeassistant/components/daikin/manifest.json | 2 +- homeassistant/components/danfoss_air/manifest.json | 2 +- homeassistant/components/darksky/manifest.json | 2 +- homeassistant/components/datadog/manifest.json | 2 +- homeassistant/components/ddwrt/manifest.json | 2 +- homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/decora/manifest.json | 2 +- homeassistant/components/decora_wifi/manifest.json | 2 +- homeassistant/components/default_config/manifest.json | 2 +- homeassistant/components/delijn/manifest.json | 2 +- homeassistant/components/deluge/manifest.json | 2 +- homeassistant/components/demo/manifest.json | 2 +- homeassistant/components/denon/manifest.json | 2 +- homeassistant/components/denonavr/manifest.json | 2 +- homeassistant/components/deutsche_bahn/manifest.json | 2 +- homeassistant/components/device_automation/manifest.json | 2 +- homeassistant/components/device_sun_light_trigger/manifest.json | 2 +- homeassistant/components/device_tracker/manifest.json | 2 +- homeassistant/components/dht/manifest.json | 2 +- homeassistant/components/dialogflow/manifest.json | 2 +- homeassistant/components/digital_ocean/manifest.json | 2 +- homeassistant/components/digitalloggers/manifest.json | 2 +- homeassistant/components/directv/manifest.json | 2 +- homeassistant/components/discogs/manifest.json | 2 +- homeassistant/components/discord/manifest.json | 2 +- homeassistant/components/discovery/manifest.json | 2 +- homeassistant/components/dlib_face_detect/manifest.json | 2 +- homeassistant/components/dlib_face_identify/manifest.json | 2 +- homeassistant/components/dlink/manifest.json | 2 +- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dnsip/manifest.json | 2 +- homeassistant/components/dominos/manifest.json | 2 +- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/doorbird/manifest.json | 2 +- homeassistant/components/dovado/manifest.json | 2 +- homeassistant/components/downloader/manifest.json | 2 +- homeassistant/components/dsmr/manifest.json | 2 +- homeassistant/components/dte_energy_bridge/manifest.json | 2 +- homeassistant/components/dublin_bus_transport/manifest.json | 2 +- homeassistant/components/duckdns/manifest.json | 2 +- homeassistant/components/duke_energy/manifest.json | 2 +- homeassistant/components/dunehd/manifest.json | 2 +- homeassistant/components/dwd_weather_warnings/manifest.json | 2 +- homeassistant/components/dweet/manifest.json | 2 +- homeassistant/components/dyson/manifest.json | 2 +- homeassistant/components/ebox/manifest.json | 2 +- homeassistant/components/ebusd/manifest.json | 2 +- homeassistant/components/ecoal_boiler/manifest.json | 2 +- homeassistant/components/ecobee/manifest.json | 2 +- homeassistant/components/econet/manifest.json | 2 +- homeassistant/components/ecovacs/manifest.json | 2 +- homeassistant/components/eddystone_temperature/manifest.json | 2 +- homeassistant/components/edimax/manifest.json | 2 +- homeassistant/components/ee_brightbox/manifest.json | 2 +- homeassistant/components/efergy/manifest.json | 2 +- homeassistant/components/egardia/manifest.json | 2 +- homeassistant/components/eight_sleep/manifest.json | 2 +- homeassistant/components/eliqonline/manifest.json | 2 +- homeassistant/components/elkm1/manifest.json | 2 +- homeassistant/components/elv/manifest.json | 2 +- homeassistant/components/emby/manifest.json | 2 +- homeassistant/components/emoncms/manifest.json | 2 +- homeassistant/components/emoncms_history/manifest.json | 2 +- homeassistant/components/emulated_hue/manifest.json | 2 +- homeassistant/components/emulated_roku/manifest.json | 2 +- homeassistant/components/enigma2/manifest.json | 2 +- homeassistant/components/enocean/manifest.json | 2 +- homeassistant/components/enphase_envoy/manifest.json | 2 +- homeassistant/components/entur_public_transport/manifest.json | 2 +- homeassistant/components/environment_canada/manifest.json | 2 +- homeassistant/components/envirophat/manifest.json | 2 +- homeassistant/components/envisalink/manifest.json | 2 +- homeassistant/components/ephember/manifest.json | 2 +- homeassistant/components/epson/manifest.json | 2 +- homeassistant/components/epsonworkforce/manifest.json | 2 +- homeassistant/components/eq3btsmart/manifest.json | 2 +- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/essent/manifest.json | 2 +- homeassistant/components/etherscan/manifest.json | 2 +- homeassistant/components/eufy/manifest.json | 2 +- homeassistant/components/everlights/manifest.json | 2 +- homeassistant/components/evohome/manifest.json | 2 +- homeassistant/components/facebook/manifest.json | 2 +- homeassistant/components/facebox/manifest.json | 2 +- homeassistant/components/fail2ban/manifest.json | 2 +- homeassistant/components/familyhub/manifest.json | 2 +- homeassistant/components/fan/manifest.json | 2 +- homeassistant/components/fastdotcom/manifest.json | 2 +- homeassistant/components/feedreader/manifest.json | 2 +- homeassistant/components/ffmpeg/manifest.json | 2 +- homeassistant/components/ffmpeg_motion/manifest.json | 2 +- homeassistant/components/ffmpeg_noise/manifest.json | 2 +- homeassistant/components/fibaro/manifest.json | 2 +- homeassistant/components/fido/manifest.json | 2 +- homeassistant/components/file/manifest.json | 2 +- homeassistant/components/filesize/manifest.json | 2 +- homeassistant/components/filter/manifest.json | 2 +- homeassistant/components/fints/manifest.json | 2 +- homeassistant/components/fitbit/manifest.json | 2 +- homeassistant/components/fixer/manifest.json | 2 +- homeassistant/components/fleetgo/manifest.json | 2 +- homeassistant/components/flexit/manifest.json | 2 +- homeassistant/components/flic/manifest.json | 2 +- homeassistant/components/flock/manifest.json | 2 +- homeassistant/components/flunearyou/manifest.json | 2 +- homeassistant/components/flux/manifest.json | 2 +- homeassistant/components/flux_led/manifest.json | 2 +- homeassistant/components/folder/manifest.json | 2 +- homeassistant/components/folder_watcher/manifest.json | 2 +- homeassistant/components/foobot/manifest.json | 2 +- homeassistant/components/fortigate/manifest.json | 2 +- homeassistant/components/fortios/manifest.json | 2 +- homeassistant/components/foscam/manifest.json | 2 +- homeassistant/components/foursquare/manifest.json | 2 +- homeassistant/components/free_mobile/manifest.json | 2 +- homeassistant/components/freebox/manifest.json | 2 +- homeassistant/components/freedns/manifest.json | 2 +- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/manifest.json | 2 +- homeassistant/components/fritzbox_netmonitor/manifest.json | 2 +- homeassistant/components/fritzdect/manifest.json | 2 +- homeassistant/components/fronius/manifest.json | 2 +- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/components/frontier_silicon/manifest.json | 2 +- homeassistant/components/futurenow/manifest.json | 2 +- homeassistant/components/garadget/manifest.json | 2 +- homeassistant/components/gc100/manifest.json | 2 +- homeassistant/components/gearbest/manifest.json | 2 +- homeassistant/components/geizhals/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/generic_thermostat/manifest.json | 2 +- homeassistant/components/geniushub/manifest.json | 2 +- homeassistant/components/geo_json_events/manifest.json | 2 +- homeassistant/components/geo_location/manifest.json | 2 +- homeassistant/components/geo_rss_events/manifest.json | 2 +- homeassistant/components/geofency/manifest.json | 2 +- homeassistant/components/geonetnz_quakes/manifest.json | 2 +- homeassistant/components/github/manifest.json | 2 +- homeassistant/components/gitlab_ci/manifest.json | 2 +- homeassistant/components/gitter/manifest.json | 2 +- homeassistant/components/glances/manifest.json | 2 +- homeassistant/components/gntp/manifest.json | 2 +- homeassistant/components/goalfeed/manifest.json | 2 +- homeassistant/components/gogogate2/manifest.json | 2 +- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/google_assistant/manifest.json | 2 +- homeassistant/components/google_cloud/manifest.json | 2 +- homeassistant/components/google_domains/manifest.json | 2 +- homeassistant/components/google_maps/manifest.json | 2 +- homeassistant/components/google_pubsub/manifest.json | 2 +- homeassistant/components/google_translate/manifest.json | 2 +- homeassistant/components/google_travel_time/manifest.json | 2 +- homeassistant/components/google_wifi/manifest.json | 2 +- homeassistant/components/gpmdp/manifest.json | 2 +- homeassistant/components/gpsd/manifest.json | 2 +- homeassistant/components/gpslogger/manifest.json | 2 +- homeassistant/components/graphite/manifest.json | 2 +- homeassistant/components/greeneye_monitor/manifest.json | 2 +- homeassistant/components/greenwave/manifest.json | 2 +- homeassistant/components/group/manifest.json | 2 +- homeassistant/components/growatt_server/manifest.json | 2 +- homeassistant/components/gstreamer/manifest.json | 2 +- homeassistant/components/gtfs/manifest.json | 2 +- homeassistant/components/gtt/manifest.json | 2 +- homeassistant/components/habitica/manifest.json | 2 +- homeassistant/components/hangouts/manifest.json | 2 +- homeassistant/components/harman_kardon_avr/manifest.json | 2 +- homeassistant/components/harmony/manifest.json | 2 +- homeassistant/components/haveibeenpwned/manifest.json | 2 +- homeassistant/components/hddtemp/manifest.json | 2 +- homeassistant/components/hdmi_cec/manifest.json | 2 +- homeassistant/components/heatmiser/manifest.json | 2 +- homeassistant/components/heos/manifest.json | 2 +- homeassistant/components/here_travel_time/manifest.json | 2 +- homeassistant/components/hikvision/manifest.json | 2 +- homeassistant/components/hikvisioncam/manifest.json | 2 +- homeassistant/components/hipchat/manifest.json | 2 +- homeassistant/components/history/manifest.json | 2 +- homeassistant/components/history_graph/manifest.json | 2 +- homeassistant/components/history_stats/manifest.json | 2 +- homeassistant/components/hitron_coda/manifest.json | 2 +- homeassistant/components/hive/manifest.json | 2 +- homeassistant/components/hlk_sw16/manifest.json | 2 +- homeassistant/components/homeassistant/manifest.json | 2 +- homeassistant/components/homekit/manifest.json | 2 +- homeassistant/components/homekit_controller/manifest.json | 2 +- homeassistant/components/homematic/manifest.json | 2 +- homeassistant/components/homematicip_cloud/manifest.json | 2 +- homeassistant/components/homeworks/manifest.json | 2 +- homeassistant/components/honeywell/manifest.json | 2 +- homeassistant/components/hook/manifest.json | 2 +- homeassistant/components/horizon/manifest.json | 2 +- homeassistant/components/hp_ilo/manifest.json | 2 +- homeassistant/components/html5/manifest.json | 2 +- homeassistant/components/http/manifest.json | 2 +- homeassistant/components/htu21d/manifest.json | 2 +- homeassistant/components/huawei_lte/manifest.json | 2 +- homeassistant/components/huawei_router/manifest.json | 2 +- homeassistant/components/hue/manifest.json | 2 +- homeassistant/components/hunterdouglas_powerview/manifest.json | 2 +- homeassistant/components/hydrawise/manifest.json | 2 +- homeassistant/components/hydroquebec/manifest.json | 2 +- homeassistant/components/hyperion/manifest.json | 2 +- homeassistant/components/ialarm/manifest.json | 2 +- homeassistant/components/iaqualink/manifest.json | 2 +- homeassistant/components/icloud/manifest.json | 2 +- homeassistant/components/idteck_prox/manifest.json | 2 +- homeassistant/components/ifttt/manifest.json | 2 +- homeassistant/components/iglo/manifest.json | 2 +- homeassistant/components/ign_sismologia/manifest.json | 2 +- homeassistant/components/ihc/manifest.json | 2 +- homeassistant/components/image_processing/manifest.json | 2 +- homeassistant/components/imap/manifest.json | 2 +- homeassistant/components/imap_email_content/manifest.json | 2 +- homeassistant/components/incomfort/manifest.json | 2 +- homeassistant/components/influxdb/manifest.json | 2 +- homeassistant/components/input_boolean/manifest.json | 2 +- homeassistant/components/input_datetime/manifest.json | 2 +- homeassistant/components/input_number/manifest.json | 2 +- homeassistant/components/input_select/manifest.json | 2 +- homeassistant/components/input_text/manifest.json | 2 +- homeassistant/components/insteon/manifest.json | 2 +- homeassistant/components/integration/manifest.json | 2 +- homeassistant/components/intent_script/manifest.json | 2 +- homeassistant/components/ios/manifest.json | 2 +- homeassistant/components/iota/manifest.json | 2 +- homeassistant/components/iperf3/manifest.json | 2 +- homeassistant/components/ipma/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/irish_rail_transport/manifest.json | 2 +- homeassistant/components/islamic_prayer_times/manifest.json | 2 +- homeassistant/components/iss/manifest.json | 2 +- homeassistant/components/isy994/manifest.json | 2 +- homeassistant/components/itach/manifest.json | 2 +- homeassistant/components/itunes/manifest.json | 2 +- homeassistant/components/izone/manifest.json | 2 +- homeassistant/components/jewish_calendar/manifest.json | 2 +- homeassistant/components/joaoapps_join/manifest.json | 2 +- homeassistant/components/juicenet/manifest.json | 2 +- homeassistant/components/kaiterra/manifest.json | 2 +- homeassistant/components/kankun/manifest.json | 2 +- homeassistant/components/keba/manifest.json | 2 +- homeassistant/components/keenetic_ndms2/manifest.json | 2 +- homeassistant/components/keyboard/manifest.json | 2 +- homeassistant/components/keyboard_remote/manifest.json | 2 +- homeassistant/components/kira/manifest.json | 2 +- homeassistant/components/kiwi/manifest.json | 2 +- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/kodi/manifest.json | 2 +- homeassistant/components/konnected/manifest.json | 2 +- homeassistant/components/kwb/manifest.json | 2 +- homeassistant/components/lacrosse/manifest.json | 2 +- homeassistant/components/lametric/manifest.json | 2 +- homeassistant/components/lannouncer/manifest.json | 2 +- homeassistant/components/lastfm/manifest.json | 2 +- homeassistant/components/launch_library/manifest.json | 2 +- homeassistant/components/lcn/manifest.json | 2 +- homeassistant/components/lg_netcast/manifest.json | 2 +- homeassistant/components/lg_soundbar/manifest.json | 2 +- homeassistant/components/life360/manifest.json | 2 +- homeassistant/components/lifx/manifest.json | 2 +- homeassistant/components/lifx_cloud/manifest.json | 2 +- homeassistant/components/lifx_legacy/manifest.json | 2 +- homeassistant/components/light/manifest.json | 2 +- homeassistant/components/lightwave/manifest.json | 2 +- homeassistant/components/limitlessled/manifest.json | 2 +- homeassistant/components/linksys_smart/manifest.json | 2 +- homeassistant/components/linky/manifest.json | 2 +- homeassistant/components/linode/manifest.json | 2 +- homeassistant/components/linux_battery/manifest.json | 2 +- homeassistant/components/lirc/manifest.json | 2 +- homeassistant/components/litejet/manifest.json | 2 +- homeassistant/components/liveboxplaytv/manifest.json | 2 +- homeassistant/components/llamalab_automate/manifest.json | 2 +- homeassistant/components/local_file/manifest.json | 2 +- homeassistant/components/locative/manifest.json | 2 +- homeassistant/components/lock/manifest.json | 2 +- homeassistant/components/lockitron/manifest.json | 2 +- homeassistant/components/logbook/manifest.json | 2 +- homeassistant/components/logentries/manifest.json | 2 +- homeassistant/components/logger/manifest.json | 2 +- homeassistant/components/logi_circle/manifest.json | 2 +- homeassistant/components/london_air/manifest.json | 2 +- homeassistant/components/london_underground/manifest.json | 2 +- homeassistant/components/loopenergy/manifest.json | 2 +- homeassistant/components/lovelace/manifest.json | 2 +- homeassistant/components/luci/manifest.json | 2 +- homeassistant/components/luftdaten/manifest.json | 2 +- homeassistant/components/lupusec/manifest.json | 2 +- homeassistant/components/lutron/manifest.json | 2 +- homeassistant/components/lutron_caseta/manifest.json | 2 +- homeassistant/components/lw12wifi/manifest.json | 2 +- homeassistant/components/lyft/manifest.json | 2 +- homeassistant/components/magicseaweed/manifest.json | 2 +- homeassistant/components/mailbox/manifest.json | 2 +- homeassistant/components/mailgun/manifest.json | 2 +- homeassistant/components/manual/manifest.json | 2 +- homeassistant/components/manual_mqtt/manifest.json | 2 +- homeassistant/components/map/manifest.json | 2 +- homeassistant/components/marytts/manifest.json | 2 +- homeassistant/components/mastodon/manifest.json | 2 +- homeassistant/components/matrix/manifest.json | 2 +- homeassistant/components/maxcube/manifest.json | 2 +- homeassistant/components/mcp23017/manifest.json | 2 +- homeassistant/components/media_extractor/manifest.json | 2 +- homeassistant/components/media_player/manifest.json | 2 +- homeassistant/components/mediaroom/manifest.json | 2 +- homeassistant/components/melissa/manifest.json | 2 +- homeassistant/components/meraki/manifest.json | 2 +- homeassistant/components/message_bird/manifest.json | 2 +- homeassistant/components/met/manifest.json | 2 +- homeassistant/components/meteo_france/manifest.json | 2 +- homeassistant/components/meteoalarm/manifest.json | 2 +- homeassistant/components/metoffice/manifest.json | 2 +- homeassistant/components/mfi/manifest.json | 2 +- homeassistant/components/mhz19/manifest.json | 2 +- homeassistant/components/microsoft/manifest.json | 2 +- homeassistant/components/microsoft_face/manifest.json | 2 +- homeassistant/components/microsoft_face_detect/manifest.json | 2 +- homeassistant/components/microsoft_face_identify/manifest.json | 2 +- homeassistant/components/miflora/manifest.json | 2 +- homeassistant/components/mikrotik/manifest.json | 2 +- homeassistant/components/mill/manifest.json | 2 +- homeassistant/components/min_max/manifest.json | 2 +- homeassistant/components/minio/manifest.json | 2 +- homeassistant/components/mitemp_bt/manifest.json | 2 +- homeassistant/components/mjpeg/manifest.json | 2 +- homeassistant/components/mobile_app/manifest.json | 2 +- homeassistant/components/mochad/manifest.json | 2 +- homeassistant/components/modbus/manifest.json | 2 +- homeassistant/components/modem_callerid/manifest.json | 2 +- homeassistant/components/mold_indicator/manifest.json | 2 +- homeassistant/components/monoprice/manifest.json | 2 +- homeassistant/components/moon/manifest.json | 2 +- homeassistant/components/mopar/manifest.json | 2 +- homeassistant/components/mpchc/manifest.json | 2 +- homeassistant/components/mpd/manifest.json | 2 +- homeassistant/components/mqtt/manifest.json | 2 +- homeassistant/components/mqtt_eventstream/manifest.json | 2 +- homeassistant/components/mqtt_json/manifest.json | 2 +- homeassistant/components/mqtt_room/manifest.json | 2 +- homeassistant/components/mqtt_statestream/manifest.json | 2 +- homeassistant/components/mvglive/manifest.json | 2 +- homeassistant/components/mychevy/manifest.json | 2 +- homeassistant/components/mycroft/manifest.json | 2 +- homeassistant/components/myq/manifest.json | 2 +- homeassistant/components/mysensors/manifest.json | 2 +- homeassistant/components/mystrom/manifest.json | 2 +- homeassistant/components/mythicbeastsdns/manifest.json | 2 +- homeassistant/components/n26/manifest.json | 2 +- homeassistant/components/nad/manifest.json | 2 +- homeassistant/components/namecheapdns/manifest.json | 2 +- homeassistant/components/nanoleaf/manifest.json | 2 +- homeassistant/components/neato/manifest.json | 2 +- homeassistant/components/nederlandse_spoorwegen/manifest.json | 2 +- homeassistant/components/nello/manifest.json | 2 +- homeassistant/components/ness_alarm/manifest.json | 2 +- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/netatmo/manifest.json | 2 +- homeassistant/components/netdata/manifest.json | 2 +- homeassistant/components/netgear/manifest.json | 2 +- homeassistant/components/netgear_lte/manifest.json | 2 +- homeassistant/components/netio/manifest.json | 2 +- homeassistant/components/neurio_energy/manifest.json | 2 +- homeassistant/components/nextbus/manifest.json | 2 +- homeassistant/components/nfandroidtv/manifest.json | 2 +- homeassistant/components/niko_home_control/manifest.json | 2 +- homeassistant/components/nilu/manifest.json | 2 +- homeassistant/components/nissan_leaf/manifest.json | 2 +- homeassistant/components/nmap_tracker/manifest.json | 2 +- homeassistant/components/nmbs/manifest.json | 2 +- homeassistant/components/no_ip/manifest.json | 2 +- homeassistant/components/noaa_tides/manifest.json | 2 +- homeassistant/components/norway_air/manifest.json | 2 +- homeassistant/components/notify/manifest.json | 2 +- homeassistant/components/notion/manifest.json | 2 +- homeassistant/components/nsw_fuel_station/manifest.json | 2 +- .../components/nsw_rural_fire_service_feed/manifest.json | 2 +- homeassistant/components/nuheat/manifest.json | 2 +- homeassistant/components/nuimo_controller/manifest.json | 2 +- homeassistant/components/nuki/manifest.json | 2 +- homeassistant/components/nut/manifest.json | 2 +- homeassistant/components/nws/manifest.json | 2 +- homeassistant/components/nx584/manifest.json | 2 +- homeassistant/components/nzbget/manifest.json | 2 +- homeassistant/components/oasa_telematics/manifest.json | 2 +- homeassistant/components/obihai/manifest.json | 2 +- homeassistant/components/octoprint/manifest.json | 2 +- homeassistant/components/oem/manifest.json | 2 +- homeassistant/components/ohmconnect/manifest.json | 2 +- homeassistant/components/ombi/manifest.json | 2 +- homeassistant/components/onboarding/manifest.json | 2 +- homeassistant/components/onewire/manifest.json | 2 +- homeassistant/components/onkyo/manifest.json | 2 +- homeassistant/components/onvif/manifest.json | 2 +- homeassistant/components/openalpr_cloud/manifest.json | 2 +- homeassistant/components/openalpr_local/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/openevse/manifest.json | 2 +- homeassistant/components/openexchangerates/manifest.json | 2 +- homeassistant/components/opengarage/manifest.json | 2 +- homeassistant/components/openhardwaremonitor/manifest.json | 2 +- homeassistant/components/openhome/manifest.json | 2 +- homeassistant/components/opensensemap/manifest.json | 2 +- homeassistant/components/opensky/manifest.json | 2 +- homeassistant/components/opentherm_gw/manifest.json | 2 +- homeassistant/components/openuv/manifest.json | 2 +- homeassistant/components/openweathermap/manifest.json | 2 +- homeassistant/components/opple/manifest.json | 2 +- homeassistant/components/orangepi_gpio/manifest.json | 2 +- homeassistant/components/orvibo/manifest.json | 2 +- homeassistant/components/osramlightify/manifest.json | 2 +- homeassistant/components/otp/manifest.json | 2 +- homeassistant/components/owlet/manifest.json | 2 +- homeassistant/components/owntracks/manifest.json | 2 +- homeassistant/components/panasonic_bluray/manifest.json | 2 +- homeassistant/components/panasonic_viera/manifest.json | 2 +- homeassistant/components/pandora/manifest.json | 2 +- homeassistant/components/panel_custom/manifest.json | 2 +- homeassistant/components/panel_iframe/manifest.json | 2 +- homeassistant/components/pencom/manifest.json | 2 +- homeassistant/components/persistent_notification/manifest.json | 2 +- homeassistant/components/person/manifest.json | 2 +- homeassistant/components/philips_js/manifest.json | 2 +- homeassistant/components/pi_hole/manifest.json | 2 +- homeassistant/components/picotts/manifest.json | 2 +- homeassistant/components/piglow/manifest.json | 2 +- homeassistant/components/pilight/manifest.json | 2 +- homeassistant/components/ping/manifest.json | 2 +- homeassistant/components/pioneer/manifest.json | 2 +- homeassistant/components/pjlink/manifest.json | 2 +- homeassistant/components/plaato/manifest.json | 2 +- homeassistant/components/plant/manifest.json | 2 +- homeassistant/components/plex/manifest.json | 2 +- homeassistant/components/plugwise/manifest.json | 2 +- homeassistant/components/plum_lightpad/manifest.json | 2 +- homeassistant/components/pocketcasts/manifest.json | 2 +- homeassistant/components/point/manifest.json | 2 +- homeassistant/components/postnl/manifest.json | 2 +- homeassistant/components/prezzibenzina/manifest.json | 2 +- homeassistant/components/proliphix/manifest.json | 2 +- homeassistant/components/prometheus/manifest.json | 2 +- homeassistant/components/prowl/manifest.json | 2 +- homeassistant/components/proximity/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/ps4/manifest.json | 2 +- homeassistant/components/ptvsd/manifest.json | 2 +- homeassistant/components/pulseaudio_loopback/manifest.json | 2 +- homeassistant/components/push/manifest.json | 2 +- homeassistant/components/pushbullet/manifest.json | 2 +- homeassistant/components/pushetta/manifest.json | 2 +- homeassistant/components/pushover/manifest.json | 2 +- homeassistant/components/pushsafer/manifest.json | 2 +- homeassistant/components/pvoutput/manifest.json | 2 +- homeassistant/components/pyload/manifest.json | 2 +- homeassistant/components/python_script/manifest.json | 2 +- homeassistant/components/qbittorrent/manifest.json | 2 +- homeassistant/components/qld_bushfire/manifest.json | 2 +- homeassistant/components/qnap/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/quantum_gateway/manifest.json | 2 +- homeassistant/components/qwikswitch/manifest.json | 2 +- homeassistant/components/rachio/manifest.json | 2 +- homeassistant/components/radarr/manifest.json | 2 +- homeassistant/components/radiotherm/manifest.json | 2 +- homeassistant/components/rainbird/manifest.json | 2 +- homeassistant/components/raincloud/manifest.json | 2 +- homeassistant/components/rainforest_eagle/manifest.json | 2 +- homeassistant/components/rainmachine/manifest.json | 2 +- homeassistant/components/random/manifest.json | 2 +- homeassistant/components/raspihats/manifest.json | 2 +- homeassistant/components/raspyrfm/manifest.json | 2 +- homeassistant/components/recollect_waste/manifest.json | 2 +- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/recswitch/manifest.json | 2 +- homeassistant/components/reddit/manifest.json | 2 +- homeassistant/components/rejseplanen/manifest.json | 2 +- homeassistant/components/remember_the_milk/manifest.json | 2 +- homeassistant/components/remote/manifest.json | 2 +- homeassistant/components/remote_rpi_gpio/manifest.json | 2 +- homeassistant/components/repetier/manifest.json | 2 +- homeassistant/components/rest/manifest.json | 2 +- homeassistant/components/rest_command/manifest.json | 2 +- homeassistant/components/rflink/manifest.json | 2 +- homeassistant/components/rfxtrx/manifest.json | 2 +- homeassistant/components/ring/manifest.json | 2 +- homeassistant/components/ripple/manifest.json | 2 +- homeassistant/components/rmvtransport/manifest.json | 2 +- homeassistant/components/rocketchat/manifest.json | 2 +- homeassistant/components/roku/manifest.json | 2 +- homeassistant/components/roomba/manifest.json | 2 +- homeassistant/components/route53/manifest.json | 2 +- homeassistant/components/rova/manifest.json | 2 +- homeassistant/components/rpi_camera/manifest.json | 2 +- homeassistant/components/rpi_gpio/manifest.json | 2 +- homeassistant/components/rpi_gpio_pwm/manifest.json | 2 +- homeassistant/components/rpi_pfio/manifest.json | 2 +- homeassistant/components/rpi_rf/manifest.json | 2 +- homeassistant/components/rss_feed_template/manifest.json | 2 +- homeassistant/components/rtorrent/manifest.json | 2 +- homeassistant/components/russound_rio/manifest.json | 2 +- homeassistant/components/russound_rnet/manifest.json | 2 +- homeassistant/components/sabnzbd/manifest.json | 2 +- homeassistant/components/saj/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/satel_integra/manifest.json | 2 +- homeassistant/components/scene/manifest.json | 2 +- homeassistant/components/scrape/manifest.json | 2 +- homeassistant/components/script/manifest.json | 2 +- homeassistant/components/scsgate/manifest.json | 2 +- homeassistant/components/season/manifest.json | 2 +- homeassistant/components/sendgrid/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- homeassistant/components/sensehat/manifest.json | 2 +- homeassistant/components/sensibo/manifest.json | 2 +- homeassistant/components/sensor/manifest.json | 2 +- homeassistant/components/serial/manifest.json | 2 +- homeassistant/components/serial_pm/manifest.json | 2 +- homeassistant/components/sesame/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/seventeentrack/manifest.json | 2 +- homeassistant/components/shell_command/manifest.json | 2 +- homeassistant/components/shiftr/manifest.json | 2 +- homeassistant/components/shodan/manifest.json | 2 +- homeassistant/components/shopping_list/manifest.json | 2 +- homeassistant/components/sht31/manifest.json | 2 +- homeassistant/components/sigfox/manifest.json | 2 +- homeassistant/components/simplepush/manifest.json | 2 +- homeassistant/components/simplisafe/manifest.json | 2 +- homeassistant/components/simulated/manifest.json | 2 +- homeassistant/components/sisyphus/manifest.json | 2 +- homeassistant/components/sky_hub/manifest.json | 2 +- homeassistant/components/skybeacon/manifest.json | 2 +- homeassistant/components/skybell/manifest.json | 2 +- homeassistant/components/slack/manifest.json | 2 +- homeassistant/components/sleepiq/manifest.json | 2 +- homeassistant/components/slide/manifest.json | 2 +- homeassistant/components/sma/manifest.json | 2 +- homeassistant/components/smappee/manifest.json | 2 +- homeassistant/components/smarthab/manifest.json | 2 +- homeassistant/components/smartthings/manifest.json | 2 +- homeassistant/components/smarty/manifest.json | 2 +- homeassistant/components/smhi/manifest.json | 2 +- homeassistant/components/smtp/manifest.json | 2 +- homeassistant/components/snapcast/manifest.json | 2 +- homeassistant/components/snips/manifest.json | 2 +- homeassistant/components/snmp/manifest.json | 2 +- homeassistant/components/sochain/manifest.json | 2 +- homeassistant/components/socialblade/manifest.json | 2 +- homeassistant/components/solaredge/manifest.json | 2 +- homeassistant/components/solaredge_local/manifest.json | 2 +- homeassistant/components/solax/manifest.json | 2 +- homeassistant/components/somfy/manifest.json | 2 +- homeassistant/components/somfy_mylink/manifest.json | 2 +- homeassistant/components/sonarr/manifest.json | 2 +- homeassistant/components/songpal/manifest.json | 2 +- homeassistant/components/sonos/manifest.json | 2 +- homeassistant/components/sony_projector/manifest.json | 2 +- homeassistant/components/soundtouch/manifest.json | 2 +- homeassistant/components/spaceapi/manifest.json | 2 +- homeassistant/components/spc/manifest.json | 2 +- homeassistant/components/speedtestdotnet/manifest.json | 2 +- homeassistant/components/spider/manifest.json | 2 +- homeassistant/components/splunk/manifest.json | 2 +- homeassistant/components/spotcrime/manifest.json | 2 +- homeassistant/components/spotify/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/components/squeezebox/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/starlingbank/manifest.json | 2 +- homeassistant/components/startca/manifest.json | 2 +- homeassistant/components/statistics/manifest.json | 2 +- homeassistant/components/statsd/manifest.json | 2 +- homeassistant/components/steam_online/manifest.json | 2 +- homeassistant/components/stiebel_eltron/manifest.json | 2 +- homeassistant/components/stream/manifest.json | 2 +- homeassistant/components/streamlabswater/manifest.json | 2 +- homeassistant/components/stride/manifest.json | 2 +- homeassistant/components/suez_water/manifest.json | 2 +- homeassistant/components/sun/manifest.json | 2 +- homeassistant/components/supervisord/manifest.json | 2 +- homeassistant/components/supla/manifest.json | 2 +- homeassistant/components/swiss_hydrological_data/manifest.json | 2 +- homeassistant/components/swiss_public_transport/manifest.json | 2 +- homeassistant/components/swisscom/manifest.json | 2 +- homeassistant/components/switch/manifest.json | 2 +- homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switcher_kis/manifest.json | 2 +- homeassistant/components/switchmate/manifest.json | 2 +- homeassistant/components/syncthru/manifest.json | 2 +- homeassistant/components/synology/manifest.json | 2 +- homeassistant/components/synology_chat/manifest.json | 2 +- homeassistant/components/synology_srm/manifest.json | 2 +- homeassistant/components/synologydsm/manifest.json | 2 +- homeassistant/components/syslog/manifest.json | 2 +- homeassistant/components/system_health/manifest.json | 2 +- homeassistant/components/system_log/manifest.json | 2 +- homeassistant/components/systemmonitor/manifest.json | 2 +- homeassistant/components/tado/manifest.json | 2 +- homeassistant/components/tahoma/manifest.json | 2 +- homeassistant/components/tank_utility/manifest.json | 2 +- homeassistant/components/tapsaff/manifest.json | 2 +- homeassistant/components/tautulli/manifest.json | 2 +- homeassistant/components/tcp/manifest.json | 2 +- homeassistant/components/ted5000/manifest.json | 2 +- homeassistant/components/teksavvy/manifest.json | 2 +- homeassistant/components/telegram/manifest.json | 2 +- homeassistant/components/telegram_bot/manifest.json | 2 +- homeassistant/components/tellduslive/manifest.json | 2 +- homeassistant/components/tellstick/manifest.json | 2 +- homeassistant/components/telnet/manifest.json | 2 +- homeassistant/components/temper/manifest.json | 2 +- homeassistant/components/template/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/tesla/manifest.json | 2 +- homeassistant/components/tfiac/manifest.json | 2 +- homeassistant/components/thermoworks_smoke/manifest.json | 2 +- homeassistant/components/thethingsnetwork/manifest.json | 2 +- homeassistant/components/thingspeak/manifest.json | 2 +- homeassistant/components/thinkingcleaner/manifest.json | 2 +- homeassistant/components/thomson/manifest.json | 2 +- homeassistant/components/threshold/manifest.json | 2 +- homeassistant/components/tibber/manifest.json | 2 +- homeassistant/components/tikteck/manifest.json | 2 +- homeassistant/components/tile/manifest.json | 2 +- homeassistant/components/time_date/manifest.json | 2 +- homeassistant/components/timer/manifest.json | 2 +- homeassistant/components/tod/manifest.json | 2 +- homeassistant/components/todoist/manifest.json | 2 +- homeassistant/components/tof/manifest.json | 2 +- homeassistant/components/tomato/manifest.json | 2 +- homeassistant/components/toon/manifest.json | 2 +- homeassistant/components/torque/manifest.json | 2 +- homeassistant/components/totalconnect/manifest.json | 2 +- homeassistant/components/touchline/manifest.json | 2 +- homeassistant/components/tplink/manifest.json | 2 +- homeassistant/components/tplink_lte/manifest.json | 2 +- homeassistant/components/traccar/manifest.json | 2 +- homeassistant/components/trackr/manifest.json | 2 +- homeassistant/components/tradfri/manifest.json | 2 +- homeassistant/components/trafikverket_train/manifest.json | 2 +- .../components/trafikverket_weatherstation/manifest.json | 2 +- homeassistant/components/transmission/manifest.json | 2 +- homeassistant/components/transport_nsw/manifest.json | 2 +- homeassistant/components/travisci/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/components/tts/manifest.json | 2 +- homeassistant/components/tuya/manifest.json | 2 +- homeassistant/components/twentemilieu/manifest.json | 2 +- homeassistant/components/twilio/manifest.json | 2 +- homeassistant/components/twilio_call/manifest.json | 2 +- homeassistant/components/twilio_sms/manifest.json | 2 +- homeassistant/components/twitch/manifest.json | 2 +- homeassistant/components/twitter/manifest.json | 2 +- homeassistant/components/ubee/manifest.json | 2 +- homeassistant/components/ubus/manifest.json | 2 +- homeassistant/components/ue_smart_radio/manifest.json | 2 +- homeassistant/components/uk_transport/manifest.json | 2 +- homeassistant/components/unifi/manifest.json | 2 +- homeassistant/components/unifi_direct/manifest.json | 2 +- homeassistant/components/universal/manifest.json | 2 +- homeassistant/components/upc_connect/manifest.json | 2 +- homeassistant/components/upcloud/manifest.json | 2 +- homeassistant/components/updater/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/uptime/manifest.json | 2 +- homeassistant/components/uptimerobot/manifest.json | 2 +- homeassistant/components/uscis/manifest.json | 2 +- homeassistant/components/usgs_earthquakes_feed/manifest.json | 2 +- homeassistant/components/utility_meter/manifest.json | 2 +- homeassistant/components/uvc/manifest.json | 2 +- homeassistant/components/vacuum/manifest.json | 2 +- homeassistant/components/vallox/manifest.json | 2 +- homeassistant/components/vasttrafik/manifest.json | 2 +- homeassistant/components/velbus/manifest.json | 2 +- homeassistant/components/velux/manifest.json | 2 +- homeassistant/components/venstar/manifest.json | 2 +- homeassistant/components/vera/manifest.json | 2 +- homeassistant/components/verisure/manifest.json | 2 +- homeassistant/components/version/manifest.json | 2 +- homeassistant/components/vesync/manifest.json | 2 +- homeassistant/components/viaggiatreno/manifest.json | 2 +- homeassistant/components/vicare/manifest.json | 2 +- homeassistant/components/vivotek/manifest.json | 2 +- homeassistant/components/vizio/manifest.json | 2 +- homeassistant/components/vlc/manifest.json | 2 +- homeassistant/components/vlc_telnet/manifest.json | 2 +- homeassistant/components/voicerss/manifest.json | 2 +- homeassistant/components/volkszaehler/manifest.json | 2 +- homeassistant/components/volumio/manifest.json | 2 +- homeassistant/components/volvooncall/manifest.json | 2 +- homeassistant/components/vultr/manifest.json | 2 +- homeassistant/components/w800rf32/manifest.json | 2 +- homeassistant/components/wake_on_lan/manifest.json | 2 +- homeassistant/components/waqi/manifest.json | 2 +- homeassistant/components/water_heater/manifest.json | 2 +- homeassistant/components/waterfurnace/manifest.json | 2 +- homeassistant/components/watson_iot/manifest.json | 2 +- homeassistant/components/watson_tts/manifest.json | 2 +- homeassistant/components/waze_travel_time/manifest.json | 2 +- homeassistant/components/weather/manifest.json | 2 +- homeassistant/components/webhook/manifest.json | 2 +- homeassistant/components/weblink/manifest.json | 2 +- homeassistant/components/webostv/manifest.json | 2 +- homeassistant/components/websocket_api/manifest.json | 2 +- homeassistant/components/wemo/manifest.json | 2 +- homeassistant/components/whois/manifest.json | 2 +- homeassistant/components/wink/manifest.json | 2 +- homeassistant/components/wirelesstag/manifest.json | 2 +- homeassistant/components/withings/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- homeassistant/components/worldclock/manifest.json | 2 +- homeassistant/components/worldtidesinfo/manifest.json | 2 +- homeassistant/components/worxlandroid/manifest.json | 2 +- homeassistant/components/wsdot/manifest.json | 2 +- homeassistant/components/wunderground/manifest.json | 2 +- homeassistant/components/wunderlist/manifest.json | 2 +- homeassistant/components/wwlln/manifest.json | 2 +- homeassistant/components/x10/manifest.json | 2 +- homeassistant/components/xbox_live/manifest.json | 2 +- homeassistant/components/xeoma/manifest.json | 2 +- homeassistant/components/xfinity/manifest.json | 2 +- homeassistant/components/xiaomi/manifest.json | 2 +- homeassistant/components/xiaomi_aqara/manifest.json | 2 +- homeassistant/components/xiaomi_miio/manifest.json | 2 +- homeassistant/components/xiaomi_tv/manifest.json | 2 +- homeassistant/components/xmpp/manifest.json | 2 +- homeassistant/components/xs1/manifest.json | 2 +- homeassistant/components/yale_smart_alarm/manifest.json | 2 +- homeassistant/components/yamaha/manifest.json | 2 +- homeassistant/components/yamaha_musiccast/manifest.json | 2 +- homeassistant/components/yandex_transport/manifest.json | 2 +- homeassistant/components/yandextts/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/components/yeelightsunflower/manifest.json | 2 +- homeassistant/components/yessssms/manifest.json | 2 +- homeassistant/components/yi/manifest.json | 2 +- homeassistant/components/yr/manifest.json | 2 +- homeassistant/components/yweather/manifest.json | 2 +- homeassistant/components/zabbix/manifest.json | 2 +- homeassistant/components/zamg/manifest.json | 2 +- homeassistant/components/zengge/manifest.json | 2 +- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/components/zestimate/manifest.json | 2 +- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zhong_hong/manifest.json | 2 +- homeassistant/components/zigbee/manifest.json | 2 +- homeassistant/components/ziggo_mediabox_xl/manifest.json | 2 +- homeassistant/components/zone/manifest.json | 2 +- homeassistant/components/zoneminder/manifest.json | 2 +- homeassistant/components/zwave/manifest.json | 2 +- 874 files changed, 874 insertions(+), 874 deletions(-) diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 49e0c46fd553ba..793c19cc466e63 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -1,7 +1,7 @@ { "domain": "abode", "name": "Abode", - "documentation": "https://www.home-assistant.io/components/abode", + "documentation": "https://www.home-assistant.io/integrations/abode", "requirements": [ "abodepy==0.15.0" ], diff --git a/homeassistant/components/acer_projector/manifest.json b/homeassistant/components/acer_projector/manifest.json index 4b8d6967491571..9f712ba5c7eed5 100644 --- a/homeassistant/components/acer_projector/manifest.json +++ b/homeassistant/components/acer_projector/manifest.json @@ -1,7 +1,7 @@ { "domain": "acer_projector", "name": "Acer projector", - "documentation": "https://www.home-assistant.io/components/acer_projector", + "documentation": "https://www.home-assistant.io/integrations/acer_projector", "requirements": [ "pyserial==3.1.1" ], diff --git a/homeassistant/components/actiontec/manifest.json b/homeassistant/components/actiontec/manifest.json index e233f430cfcbbb..ddb4954794cb5d 100644 --- a/homeassistant/components/actiontec/manifest.json +++ b/homeassistant/components/actiontec/manifest.json @@ -1,7 +1,7 @@ { "domain": "actiontec", "name": "Actiontec", - "documentation": "https://www.home-assistant.io/components/actiontec", + "documentation": "https://www.home-assistant.io/integrations/actiontec", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index 0063f1ec37064c..f207e6dff09b06 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -2,7 +2,7 @@ "domain": "adguard", "name": "AdGuard Home", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/adguard", + "documentation": "https://www.home-assistant.io/integrations/adguard", "requirements": [ "adguardhome==0.2.1" ], diff --git a/homeassistant/components/ads/manifest.json b/homeassistant/components/ads/manifest.json index 0c759f0ad60c57..cf3e621fd071ca 100644 --- a/homeassistant/components/ads/manifest.json +++ b/homeassistant/components/ads/manifest.json @@ -1,7 +1,7 @@ { "domain": "ads", "name": "Ads", - "documentation": "https://www.home-assistant.io/components/ads", + "documentation": "https://www.home-assistant.io/integrations/ads", "requirements": [ "pyads==3.0.7" ], diff --git a/homeassistant/components/aftership/manifest.json b/homeassistant/components/aftership/manifest.json index b9ee8939dc4631..a7e8aeb8debd11 100644 --- a/homeassistant/components/aftership/manifest.json +++ b/homeassistant/components/aftership/manifest.json @@ -1,7 +1,7 @@ { "domain": "aftership", "name": "Aftership", - "documentation": "https://www.home-assistant.io/components/aftership", + "documentation": "https://www.home-assistant.io/integrations/aftership", "requirements": [ "pyaftership==0.1.2" ], diff --git a/homeassistant/components/air_quality/manifest.json b/homeassistant/components/air_quality/manifest.json index 5bfe85547ffce4..17fa14a9cfac0a 100644 --- a/homeassistant/components/air_quality/manifest.json +++ b/homeassistant/components/air_quality/manifest.json @@ -1,7 +1,7 @@ { "domain": "air_quality", "name": "Air quality", - "documentation": "https://www.home-assistant.io/components/air_quality", + "documentation": "https://www.home-assistant.io/integrations/air_quality", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index ddb109a99b07e5..e7ea23a43a1a63 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -1,7 +1,7 @@ { "domain": "airvisual", "name": "Airvisual", - "documentation": "https://www.home-assistant.io/components/airvisual", + "documentation": "https://www.home-assistant.io/integrations/airvisual", "requirements": [ "pyairvisual==3.0.1" ], diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 0681d5df38b248..7d440407427e45 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -1,7 +1,7 @@ { "domain": "aladdin_connect", "name": "Aladdin connect", - "documentation": "https://www.home-assistant.io/components/aladdin_connect", + "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "requirements": [ "aladdin_connect==0.3" ], diff --git a/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant/components/alarm_control_panel/manifest.json index 95e26de53bcb3b..04ef58769dad7b 100644 --- a/homeassistant/components/alarm_control_panel/manifest.json +++ b/homeassistant/components/alarm_control_panel/manifest.json @@ -1,7 +1,7 @@ { "domain": "alarm_control_panel", "name": "Alarm control panel", - "documentation": "https://www.home-assistant.io/components/alarm_control_panel", + "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index 3e0d4112d2735f..5ab69d94cf2218 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -1,7 +1,7 @@ { "domain": "alarmdecoder", "name": "Alarmdecoder", - "documentation": "https://www.home-assistant.io/components/alarmdecoder", + "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", "requirements": [ "alarmdecoder==1.13.2" ], diff --git a/homeassistant/components/alarmdotcom/manifest.json b/homeassistant/components/alarmdotcom/manifest.json index 9d2c0a2056e362..fd5c3010ab6757 100644 --- a/homeassistant/components/alarmdotcom/manifest.json +++ b/homeassistant/components/alarmdotcom/manifest.json @@ -1,7 +1,7 @@ { "domain": "alarmdotcom", "name": "Alarmdotcom", - "documentation": "https://www.home-assistant.io/components/alarmdotcom", + "documentation": "https://www.home-assistant.io/integrations/alarmdotcom", "requirements": [ "pyalarmdotcom==0.3.2" ], diff --git a/homeassistant/components/alert/manifest.json b/homeassistant/components/alert/manifest.json index 2e27beb48e68f0..06269390753d5c 100644 --- a/homeassistant/components/alert/manifest.json +++ b/homeassistant/components/alert/manifest.json @@ -1,7 +1,7 @@ { "domain": "alert", "name": "Alert", - "documentation": "https://www.home-assistant.io/components/alert", + "documentation": "https://www.home-assistant.io/integrations/alert", "requirements": [], "dependencies": [], "after_dependencies": [ diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index e4fc9eb86805aa..c6629982d53fae 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -1,7 +1,7 @@ { "domain": "alexa", "name": "Alexa", - "documentation": "https://www.home-assistant.io/components/alexa", + "documentation": "https://www.home-assistant.io/integrations/alexa", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index dacc428ea2ee36..9ac8d1ea1e0b70 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -1,7 +1,7 @@ { "domain": "alpha_vantage", "name": "Alpha vantage", - "documentation": "https://www.home-assistant.io/components/alpha_vantage", + "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", "requirements": [ "alpha_vantage==2.1.0" ], diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index 20d443f225eb18..45e382647f8d38 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -1,7 +1,7 @@ { "domain": "amazon_polly", "name": "Amazon polly", - "documentation": "https://www.home-assistant.io/components/amazon_polly", + "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "requirements": [ "boto3==1.9.233" ], diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json index 8e5ddb924ca655..3d175165abd1fc 100644 --- a/homeassistant/components/ambiclimate/manifest.json +++ b/homeassistant/components/ambiclimate/manifest.json @@ -2,7 +2,7 @@ "domain": "ambiclimate", "name": "Ambiclimate", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ambiclimate", + "documentation": "https://www.home-assistant.io/integrations/ambiclimate", "requirements": [ "ambiclimate==0.2.1" ], diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 056930edfc7bc3..8f363ba219f99d 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -2,7 +2,7 @@ "domain": "ambient_station", "name": "Ambient station", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ambient_station", + "documentation": "https://www.home-assistant.io/integrations/ambient_station", "requirements": [ "aioambient==0.3.2" ], diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index f79ce34897b92c..4453687b895312 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -1,7 +1,7 @@ { "domain": "amcrest", "name": "Amcrest", - "documentation": "https://www.home-assistant.io/components/amcrest", + "documentation": "https://www.home-assistant.io/integrations/amcrest", "requirements": [ "amcrest==1.5.3" ], diff --git a/homeassistant/components/ampio/manifest.json b/homeassistant/components/ampio/manifest.json index d20b10b2d1509e..6bf79e27f85e3a 100644 --- a/homeassistant/components/ampio/manifest.json +++ b/homeassistant/components/ampio/manifest.json @@ -1,7 +1,7 @@ { "domain": "ampio", "name": "Ampio", - "documentation": "https://www.home-assistant.io/components/ampio", + "documentation": "https://www.home-assistant.io/integrations/ampio", "requirements": [ "asmog==0.0.6" ], diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 28909f7e053379..c9602d757510ed 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -1,7 +1,7 @@ { "domain": "android_ip_webcam", "name": "Android ip webcam", - "documentation": "https://www.home-assistant.io/components/android_ip_webcam", + "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", "requirements": [ "pydroid-ipcam==0.8" ], diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index dc2d31c23583ef..4fd3b062a10721 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -1,7 +1,7 @@ { "domain": "androidtv", "name": "Androidtv", - "documentation": "https://www.home-assistant.io/components/androidtv", + "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell==0.0.3", "androidtv==0.0.29" diff --git a/homeassistant/components/anel_pwrctrl/manifest.json b/homeassistant/components/anel_pwrctrl/manifest.json index 17802918cd2263..d4055a4068f335 100644 --- a/homeassistant/components/anel_pwrctrl/manifest.json +++ b/homeassistant/components/anel_pwrctrl/manifest.json @@ -1,7 +1,7 @@ { "domain": "anel_pwrctrl", "name": "Anel pwrctrl", - "documentation": "https://www.home-assistant.io/components/anel_pwrctrl", + "documentation": "https://www.home-assistant.io/integrations/anel_pwrctrl", "requirements": [ "anel_pwrctrl-homeassistant==0.0.1.dev2" ], diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index 9b2e3c697bb220..7c648d37b10d56 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -1,7 +1,7 @@ { "domain": "anthemav", "name": "Anthemav", - "documentation": "https://www.home-assistant.io/components/anthemav", + "documentation": "https://www.home-assistant.io/integrations/anthemav", "requirements": [ "anthemav==1.1.10" ], diff --git a/homeassistant/components/apache_kafka/manifest.json b/homeassistant/components/apache_kafka/manifest.json index ac36af7fa48fe4..fb2bd64352579f 100644 --- a/homeassistant/components/apache_kafka/manifest.json +++ b/homeassistant/components/apache_kafka/manifest.json @@ -1,7 +1,7 @@ { "domain": "apache_kafka", "name": "Apache Kafka", - "documentation": "https://www.home-assistant.io/components/apache_kafka", + "documentation": "https://www.home-assistant.io/integrations/apache_kafka", "requirements": [ "aiokafka==0.5.1" ], diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 813176728f2843..08cac545249a20 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -1,7 +1,7 @@ { "domain": "apcupsd", "name": "Apcupsd", - "documentation": "https://www.home-assistant.io/components/apcupsd", + "documentation": "https://www.home-assistant.io/integrations/apcupsd", "requirements": [ "apcaccess==0.0.13" ], diff --git a/homeassistant/components/api/manifest.json b/homeassistant/components/api/manifest.json index 25d9a76036eec6..830fc04445a855 100644 --- a/homeassistant/components/api/manifest.json +++ b/homeassistant/components/api/manifest.json @@ -1,7 +1,7 @@ { "domain": "api", "name": "Home Assistant API", - "documentation": "https://www.home-assistant.io/components/api", + "documentation": "https://www.home-assistant.io/integrations/api", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/apns/manifest.json b/homeassistant/components/apns/manifest.json index 9a310a096a5a4d..4845c45a963dce 100644 --- a/homeassistant/components/apns/manifest.json +++ b/homeassistant/components/apns/manifest.json @@ -1,7 +1,7 @@ { "domain": "apns", "name": "Apns", - "documentation": "https://www.home-assistant.io/components/apns", + "documentation": "https://www.home-assistant.io/integrations/apns", "requirements": [ "apns2==0.3.0" ], diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index c391fb0e14ba0e..f8fd2e0efa2ad1 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -1,7 +1,7 @@ { "domain": "apple_tv", "name": "Apple tv", - "documentation": "https://www.home-assistant.io/components/apple_tv", + "documentation": "https://www.home-assistant.io/integrations/apple_tv", "requirements": [ "pyatv==0.3.13" ], diff --git a/homeassistant/components/aprs/manifest.json b/homeassistant/components/aprs/manifest.json index fbe13ca85782c9..c7615817b50a91 100644 --- a/homeassistant/components/aprs/manifest.json +++ b/homeassistant/components/aprs/manifest.json @@ -1,7 +1,7 @@ { "domain": "aprs", "name": "APRS", - "documentation": "https://www.home-assistant.io/components/aprs", + "documentation": "https://www.home-assistant.io/integrations/aprs", "dependencies": [], "codeowners": ["@PhilRW"], "requirements": [ diff --git a/homeassistant/components/aqualogic/manifest.json b/homeassistant/components/aqualogic/manifest.json index 40f1805d83abec..2e5dded4af6582 100644 --- a/homeassistant/components/aqualogic/manifest.json +++ b/homeassistant/components/aqualogic/manifest.json @@ -1,7 +1,7 @@ { "domain": "aqualogic", "name": "Aqualogic", - "documentation": "https://www.home-assistant.io/components/aqualogic", + "documentation": "https://www.home-assistant.io/integrations/aqualogic", "requirements": [ "aqualogic==1.0" ], diff --git a/homeassistant/components/aquostv/manifest.json b/homeassistant/components/aquostv/manifest.json index 16865905ae984a..d4c491cb70d539 100644 --- a/homeassistant/components/aquostv/manifest.json +++ b/homeassistant/components/aquostv/manifest.json @@ -1,7 +1,7 @@ { "domain": "aquostv", "name": "Aquostv", - "documentation": "https://www.home-assistant.io/components/aquostv", + "documentation": "https://www.home-assistant.io/integrations/aquostv", "requirements": [ "sharp_aquos_rc==0.3.2" ], diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 59ab3c03d9273e..288b8fb38903e1 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -2,7 +2,7 @@ "domain": "arcam_fmj", "name": "Arcam FMJ Receiver control", "config_flow": false, - "documentation": "https://www.home-assistant.io/components/arcam_fmj", + "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", "requirements": [ "arcam-fmj==0.4.3" ], diff --git a/homeassistant/components/arduino/manifest.json b/homeassistant/components/arduino/manifest.json index cf21cbe87eafe2..3567ce71cd12cf 100644 --- a/homeassistant/components/arduino/manifest.json +++ b/homeassistant/components/arduino/manifest.json @@ -1,7 +1,7 @@ { "domain": "arduino", "name": "Arduino", - "documentation": "https://www.home-assistant.io/components/arduino", + "documentation": "https://www.home-assistant.io/integrations/arduino", "requirements": [ "PyMata==2.14" ], diff --git a/homeassistant/components/arest/manifest.json b/homeassistant/components/arest/manifest.json index d5bcf92a39dc4b..ee6b915e658a62 100644 --- a/homeassistant/components/arest/manifest.json +++ b/homeassistant/components/arest/manifest.json @@ -1,7 +1,7 @@ { "domain": "arest", "name": "Arest", - "documentation": "https://www.home-assistant.io/components/arest", + "documentation": "https://www.home-assistant.io/integrations/arest", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/arlo/manifest.json b/homeassistant/components/arlo/manifest.json index 35803d0d4f6afb..8779e051dc0e14 100644 --- a/homeassistant/components/arlo/manifest.json +++ b/homeassistant/components/arlo/manifest.json @@ -1,7 +1,7 @@ { "domain": "arlo", "name": "Arlo", - "documentation": "https://www.home-assistant.io/components/arlo", + "documentation": "https://www.home-assistant.io/integrations/arlo", "requirements": [ "pyarlo==0.2.3" ], diff --git a/homeassistant/components/aruba/manifest.json b/homeassistant/components/aruba/manifest.json index 597975619e6a4e..ccc4f1190926f6 100644 --- a/homeassistant/components/aruba/manifest.json +++ b/homeassistant/components/aruba/manifest.json @@ -1,7 +1,7 @@ { "domain": "aruba", "name": "Aruba", - "documentation": "https://www.home-assistant.io/components/aruba", + "documentation": "https://www.home-assistant.io/integrations/aruba", "requirements": [ "pexpect==4.6.0" ], diff --git a/homeassistant/components/arwn/manifest.json b/homeassistant/components/arwn/manifest.json index 1c861aa67e28bf..d84202cbac8902 100644 --- a/homeassistant/components/arwn/manifest.json +++ b/homeassistant/components/arwn/manifest.json @@ -1,7 +1,7 @@ { "domain": "arwn", "name": "Arwn", - "documentation": "https://www.home-assistant.io/components/arwn", + "documentation": "https://www.home-assistant.io/integrations/arwn", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/asterisk_cdr/manifest.json b/homeassistant/components/asterisk_cdr/manifest.json index db1308b0483d78..56018ba777bd82 100644 --- a/homeassistant/components/asterisk_cdr/manifest.json +++ b/homeassistant/components/asterisk_cdr/manifest.json @@ -1,7 +1,7 @@ { "domain": "asterisk_cdr", "name": "Asterisk cdr", - "documentation": "https://www.home-assistant.io/components/asterisk_cdr", + "documentation": "https://www.home-assistant.io/integrations/asterisk_cdr", "requirements": [], "dependencies": [ "asterisk_mbox" diff --git a/homeassistant/components/asterisk_mbox/manifest.json b/homeassistant/components/asterisk_mbox/manifest.json index bafe43c480f4f7..cf793328d932eb 100644 --- a/homeassistant/components/asterisk_mbox/manifest.json +++ b/homeassistant/components/asterisk_mbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "asterisk_mbox", "name": "Asterisk mbox", - "documentation": "https://www.home-assistant.io/components/asterisk_mbox", + "documentation": "https://www.home-assistant.io/integrations/asterisk_mbox", "requirements": [ "asterisk_mbox==0.5.0" ], diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index f36819f133ddb8..3fcdcf42ab1b3e 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -1,7 +1,7 @@ { "domain": "asuswrt", "name": "Asuswrt", - "documentation": "https://www.home-assistant.io/components/asuswrt", + "documentation": "https://www.home-assistant.io/integrations/asuswrt", "requirements": [ "aioasuswrt==1.1.21" ], diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index 621faba4fc0a33..55739cad8cc7af 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -1,7 +1,7 @@ { "domain": "atome", "name": "Atome", - "documentation": "https://www.home-assistant.io/components/atome", + "documentation": "https://www.home-assistant.io/integrations/atome", "dependencies": [], "codeowners": ["@baqs"], "requirements": ["pyatome==0.1.1"] diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index e41491c4b0ac85..ebaa564736f19f 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -1,7 +1,7 @@ { "domain": "august", "name": "August", - "documentation": "https://www.home-assistant.io/components/august", + "documentation": "https://www.home-assistant.io/integrations/august", "requirements": [ "py-august==0.7.0" ], diff --git a/homeassistant/components/aurora/manifest.json b/homeassistant/components/aurora/manifest.json index 56ba3fe935608a..204327043f9c07 100644 --- a/homeassistant/components/aurora/manifest.json +++ b/homeassistant/components/aurora/manifest.json @@ -1,7 +1,7 @@ { "domain": "aurora", "name": "Aurora", - "documentation": "https://www.home-assistant.io/components/aurora", + "documentation": "https://www.home-assistant.io/integrations/aurora", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/aurora_abb_powerone/manifest.json b/homeassistant/components/aurora_abb_powerone/manifest.json index 56325dd40af64f..f49421ea954821 100644 --- a/homeassistant/components/aurora_abb_powerone/manifest.json +++ b/homeassistant/components/aurora_abb_powerone/manifest.json @@ -1,7 +1,7 @@ { "domain": "aurora_abb_powerone", "name": "Aurora ABB Solar PV", - "documentation": "https://www.home-assistant.io/components/aurora_abb_powerone/", + "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone/", "dependencies": [], "codeowners": [ "@davet2001" diff --git a/homeassistant/components/auth/manifest.json b/homeassistant/components/auth/manifest.json index 10be545f5e14eb..1b0ab33f38155b 100644 --- a/homeassistant/components/auth/manifest.json +++ b/homeassistant/components/auth/manifest.json @@ -1,7 +1,7 @@ { "domain": "auth", "name": "Auth", - "documentation": "https://www.home-assistant.io/components/auth", + "documentation": "https://www.home-assistant.io/integrations/auth", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/automatic/manifest.json b/homeassistant/components/automatic/manifest.json index 9743835af20ab2..63cf0da0f46fb5 100644 --- a/homeassistant/components/automatic/manifest.json +++ b/homeassistant/components/automatic/manifest.json @@ -1,7 +1,7 @@ { "domain": "automatic", "name": "Automatic", - "documentation": "https://www.home-assistant.io/components/automatic", + "documentation": "https://www.home-assistant.io/integrations/automatic", "requirements": [ "aioautomatic==0.6.5" ], diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 935cc7a9175058..79a6877692ed94 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -1,7 +1,7 @@ { "domain": "automation", "name": "Automation", - "documentation": "https://www.home-assistant.io/components/automation", + "documentation": "https://www.home-assistant.io/integrations/automation", "requirements": [], "dependencies": [ "device_automation", diff --git a/homeassistant/components/avea/manifest.json b/homeassistant/components/avea/manifest.json index 273cefcbcfa83d..4fb9ab9f4205c7 100644 --- a/homeassistant/components/avea/manifest.json +++ b/homeassistant/components/avea/manifest.json @@ -1,7 +1,7 @@ { "domain": "avea", "name": "Elgato Avea", - "documentation": "https://www.home-assistant.io/components/avea", + "documentation": "https://www.home-assistant.io/integrations/avea", "dependencies": [], "codeowners": ["@pattyland"], "requirements": ["avea==1.2.8"] diff --git a/homeassistant/components/avion/manifest.json b/homeassistant/components/avion/manifest.json index e7d97f13313088..8bb8b56cb9d858 100644 --- a/homeassistant/components/avion/manifest.json +++ b/homeassistant/components/avion/manifest.json @@ -1,7 +1,7 @@ { "domain": "avion", "name": "Avion", - "documentation": "https://www.home-assistant.io/components/avion", + "documentation": "https://www.home-assistant.io/integrations/avion", "requirements": [ "avion==0.10" ], diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index dfa5bec3c00309..f4e632cb7d23d3 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -1,7 +1,7 @@ { "domain": "awair", "name": "Awair", - "documentation": "https://www.home-assistant.io/components/awair", + "documentation": "https://www.home-assistant.io/integrations/awair", "requirements": [ "python_awair==0.0.4" ], diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index a473a23f917ab0..a4543cc4b0f5b7 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -1,7 +1,7 @@ { "domain": "aws", "name": "Aws", - "documentation": "https://www.home-assistant.io/components/aws", + "documentation": "https://www.home-assistant.io/integrations/aws", "requirements": [ "aiobotocore==0.10.2" ], diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 2b1bef9081e9cd..348f6148386328 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -2,7 +2,7 @@ "domain": "axis", "name": "Axis", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/axis", + "documentation": "https://www.home-assistant.io/integrations/axis", "requirements": ["axis==25"], "dependencies": [], "zeroconf": ["_axis-video._tcp.local."], diff --git a/homeassistant/components/azure_event_hub/manifest.json b/homeassistant/components/azure_event_hub/manifest.json index e2223fc97c3b3b..0b705bddc297fc 100644 --- a/homeassistant/components/azure_event_hub/manifest.json +++ b/homeassistant/components/azure_event_hub/manifest.json @@ -1,7 +1,7 @@ { "domain": "azure_event_hub", "name": "Azure Event Hub", - "documentation": "https://www.home-assistant.io/components/azure_event_hub", + "documentation": "https://www.home-assistant.io/integrations/azure_event_hub", "requirements": ["azure-eventhub==1.3.1"], "dependencies": [], "codeowners": ["@eavanvalkenburg"] diff --git a/homeassistant/components/baidu/manifest.json b/homeassistant/components/baidu/manifest.json index 1dea1b7e37b147..756a1c5adcc1bb 100644 --- a/homeassistant/components/baidu/manifest.json +++ b/homeassistant/components/baidu/manifest.json @@ -1,7 +1,7 @@ { "domain": "baidu", "name": "Baidu", - "documentation": "https://www.home-assistant.io/components/baidu", + "documentation": "https://www.home-assistant.io/integrations/baidu", "requirements": [ "baidu-aip==1.6.6" ], diff --git a/homeassistant/components/bayesian/manifest.json b/homeassistant/components/bayesian/manifest.json index 25480ac8bdc870..7060dbd396bd03 100644 --- a/homeassistant/components/bayesian/manifest.json +++ b/homeassistant/components/bayesian/manifest.json @@ -1,7 +1,7 @@ { "domain": "bayesian", "name": "Bayesian", - "documentation": "https://www.home-assistant.io/components/bayesian", + "documentation": "https://www.home-assistant.io/integrations/bayesian", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/bbb_gpio/manifest.json b/homeassistant/components/bbb_gpio/manifest.json index 5632836bfbb623..edd6032682812a 100644 --- a/homeassistant/components/bbb_gpio/manifest.json +++ b/homeassistant/components/bbb_gpio/manifest.json @@ -1,7 +1,7 @@ { "domain": "bbb_gpio", "name": "Bbb gpio", - "documentation": "https://www.home-assistant.io/components/bbb_gpio", + "documentation": "https://www.home-assistant.io/integrations/bbb_gpio", "requirements": [ "Adafruit_BBIO==1.0.0" ], diff --git a/homeassistant/components/bbox/manifest.json b/homeassistant/components/bbox/manifest.json index 54cd9a3af64dda..15a648167c5cc4 100644 --- a/homeassistant/components/bbox/manifest.json +++ b/homeassistant/components/bbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "bbox", "name": "Bbox", - "documentation": "https://www.home-assistant.io/components/bbox", + "documentation": "https://www.home-assistant.io/integrations/bbox", "requirements": [ "pybbox==0.0.5-alpha" ], diff --git a/homeassistant/components/beewi_smartclim/manifest.json b/homeassistant/components/beewi_smartclim/manifest.json index 3e9ad732b74342..bc69efb64903fd 100644 --- a/homeassistant/components/beewi_smartclim/manifest.json +++ b/homeassistant/components/beewi_smartclim/manifest.json @@ -1,7 +1,7 @@ { "domain": "beewi_smartclim", "name": "BeeWi SmartClim BLE sensor", - "documentation": "https://www.home-assistant.io/components/beewi_smartclim", + "documentation": "https://www.home-assistant.io/integrations/beewi_smartclim", "requirements": [ "beewi_smartclim==0.0.7" ], diff --git a/homeassistant/components/bh1750/manifest.json b/homeassistant/components/bh1750/manifest.json index 90e62c783569cb..63816f967e7564 100644 --- a/homeassistant/components/bh1750/manifest.json +++ b/homeassistant/components/bh1750/manifest.json @@ -1,7 +1,7 @@ { "domain": "bh1750", "name": "Bh1750", - "documentation": "https://www.home-assistant.io/components/bh1750", + "documentation": "https://www.home-assistant.io/integrations/bh1750", "requirements": [ "i2csense==0.0.4", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/binary_sensor/manifest.json b/homeassistant/components/binary_sensor/manifest.json index d627351958d573..ea29314eb1b3de 100644 --- a/homeassistant/components/binary_sensor/manifest.json +++ b/homeassistant/components/binary_sensor/manifest.json @@ -1,7 +1,7 @@ { "domain": "binary_sensor", "name": "Binary sensor", - "documentation": "https://www.home-assistant.io/components/binary_sensor", + "documentation": "https://www.home-assistant.io/integrations/binary_sensor", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/bitcoin/manifest.json b/homeassistant/components/bitcoin/manifest.json index 85da99a68850cf..1ffc34fcd9df39 100644 --- a/homeassistant/components/bitcoin/manifest.json +++ b/homeassistant/components/bitcoin/manifest.json @@ -1,7 +1,7 @@ { "domain": "bitcoin", "name": "Bitcoin", - "documentation": "https://www.home-assistant.io/components/bitcoin", + "documentation": "https://www.home-assistant.io/integrations/bitcoin", "requirements": [ "blockchain==1.4.4" ], diff --git a/homeassistant/components/bizkaibus/manifest.json b/homeassistant/components/bizkaibus/manifest.json index 98cbbc9be56298..63c0494c2f18f5 100644 --- a/homeassistant/components/bizkaibus/manifest.json +++ b/homeassistant/components/bizkaibus/manifest.json @@ -1,7 +1,7 @@ { "domain": "bizkaibus", "name": "Bizkaibus", - "documentation": "https://www.home-assistant.io/components/bizkaibus", + "documentation": "https://www.home-assistant.io/integrations/bizkaibus", "dependencies": [], "codeowners": ["@UgaitzEtxebarria"], "requirements": ["bizkaibus==0.1.1"] diff --git a/homeassistant/components/blackbird/manifest.json b/homeassistant/components/blackbird/manifest.json index 9e3e41290ea373..c5b3a632c16ca2 100644 --- a/homeassistant/components/blackbird/manifest.json +++ b/homeassistant/components/blackbird/manifest.json @@ -1,7 +1,7 @@ { "domain": "blackbird", "name": "Blackbird", - "documentation": "https://www.home-assistant.io/components/blackbird", + "documentation": "https://www.home-assistant.io/integrations/blackbird", "requirements": [ "pyblackbird==0.5" ], diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 98c609731c6d31..a38ba0bd613b82 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -1,7 +1,7 @@ { "domain": "blink", "name": "Blink", - "documentation": "https://www.home-assistant.io/components/blink", + "documentation": "https://www.home-assistant.io/integrations/blink", "requirements": [ "blinkpy==0.14.1" ], diff --git a/homeassistant/components/blinksticklight/manifest.json b/homeassistant/components/blinksticklight/manifest.json index a5277c97d99386..0a6e25407900bd 100644 --- a/homeassistant/components/blinksticklight/manifest.json +++ b/homeassistant/components/blinksticklight/manifest.json @@ -1,7 +1,7 @@ { "domain": "blinksticklight", "name": "Blinksticklight", - "documentation": "https://www.home-assistant.io/components/blinksticklight", + "documentation": "https://www.home-assistant.io/integrations/blinksticklight", "requirements": [ "blinkstick==1.1.8" ], diff --git a/homeassistant/components/blinkt/manifest.json b/homeassistant/components/blinkt/manifest.json index c11583ed59ec3d..629bdebf27e6c4 100644 --- a/homeassistant/components/blinkt/manifest.json +++ b/homeassistant/components/blinkt/manifest.json @@ -1,7 +1,7 @@ { "domain": "blinkt", "name": "Blinkt", - "documentation": "https://www.home-assistant.io/components/blinkt", + "documentation": "https://www.home-assistant.io/integrations/blinkt", "requirements": [ "blinkt==0.1.0" ], diff --git a/homeassistant/components/blockchain/manifest.json b/homeassistant/components/blockchain/manifest.json index 8a2a9f7b71f00d..773b52e724bb9e 100644 --- a/homeassistant/components/blockchain/manifest.json +++ b/homeassistant/components/blockchain/manifest.json @@ -1,7 +1,7 @@ { "domain": "blockchain", "name": "Blockchain", - "documentation": "https://www.home-assistant.io/components/blockchain", + "documentation": "https://www.home-assistant.io/integrations/blockchain", "requirements": [ "python-blockchain-api==0.0.2" ], diff --git a/homeassistant/components/bloomsky/manifest.json b/homeassistant/components/bloomsky/manifest.json index 3a780507dd59c4..49da6534bae4ff 100644 --- a/homeassistant/components/bloomsky/manifest.json +++ b/homeassistant/components/bloomsky/manifest.json @@ -1,7 +1,7 @@ { "domain": "bloomsky", "name": "Bloomsky", - "documentation": "https://www.home-assistant.io/components/bloomsky", + "documentation": "https://www.home-assistant.io/integrations/bloomsky", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index 7731f845005ded..e64e3e61f16ea0 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -1,7 +1,7 @@ { "domain": "bluesound", "name": "Bluesound", - "documentation": "https://www.home-assistant.io/components/bluesound", + "documentation": "https://www.home-assistant.io/integrations/bluesound", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index d2f8f10290e5ea..30ed924a9dc404 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "bluetooth_le_tracker", "name": "Bluetooth le tracker", - "documentation": "https://www.home-assistant.io/components/bluetooth_le_tracker", + "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", "requirements": [ "pygatt[GATTTOOL]==4.0.1" ], diff --git a/homeassistant/components/bluetooth_tracker/manifest.json b/homeassistant/components/bluetooth_tracker/manifest.json index c853bc5a83801d..20fe51c561bf2d 100644 --- a/homeassistant/components/bluetooth_tracker/manifest.json +++ b/homeassistant/components/bluetooth_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "bluetooth_tracker", "name": "Bluetooth tracker", - "documentation": "https://www.home-assistant.io/components/bluetooth_tracker", + "documentation": "https://www.home-assistant.io/integrations/bluetooth_tracker", "requirements": [ "bt_proximity==0.2", "pybluez==0.22" diff --git a/homeassistant/components/bme280/manifest.json b/homeassistant/components/bme280/manifest.json index 2342c8418ebce2..9d01b301d432d4 100644 --- a/homeassistant/components/bme280/manifest.json +++ b/homeassistant/components/bme280/manifest.json @@ -1,7 +1,7 @@ { "domain": "bme280", "name": "Bme280", - "documentation": "https://www.home-assistant.io/components/bme280", + "documentation": "https://www.home-assistant.io/integrations/bme280", "requirements": [ "i2csense==0.0.4", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/bme680/manifest.json b/homeassistant/components/bme680/manifest.json index 976be85ca9413e..c062d14f8c9684 100644 --- a/homeassistant/components/bme680/manifest.json +++ b/homeassistant/components/bme680/manifest.json @@ -1,7 +1,7 @@ { "domain": "bme680", "name": "Bme680", - "documentation": "https://www.home-assistant.io/components/bme680", + "documentation": "https://www.home-assistant.io/integrations/bme680", "requirements": [ "bme680==1.0.5", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 0cc875c50f9f51..29366715d239a3 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -1,7 +1,7 @@ { "domain": "bmw_connected_drive", "name": "BMW Connected Drive", - "documentation": "https://www.home-assistant.io/components/bmw_connected_drive", + "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "requirements": [ "bimmer_connected==0.6.0" ], diff --git a/homeassistant/components/bom/manifest.json b/homeassistant/components/bom/manifest.json index eb1f1d8ca9428e..b2e7eb08ef6e5d 100644 --- a/homeassistant/components/bom/manifest.json +++ b/homeassistant/components/bom/manifest.json @@ -1,7 +1,7 @@ { "domain": "bom", "name": "Bom", - "documentation": "https://www.home-assistant.io/components/bom", + "documentation": "https://www.home-assistant.io/integrations/bom", "requirements": [ "bomradarloop==0.1.3" ], diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 52e8e1bec76bc7..e49e45865c4611 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -1,7 +1,7 @@ { "domain": "braviatv", "name": "Braviatv", - "documentation": "https://www.home-assistant.io/components/braviatv", + "documentation": "https://www.home-assistant.io/integrations/braviatv", "requirements": [ "braviarc-homeassistant==0.3.7.dev0", "getmac==0.8.1" diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 45ed2003026fd2..d77c32966b19e0 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -1,7 +1,7 @@ { "domain": "broadlink", "name": "Broadlink", - "documentation": "https://www.home-assistant.io/components/broadlink", + "documentation": "https://www.home-assistant.io/integrations/broadlink", "requirements": [ "broadlink==0.11.1" ], diff --git a/homeassistant/components/brottsplatskartan/manifest.json b/homeassistant/components/brottsplatskartan/manifest.json index d3b0657fed82e8..f3dd46c96fc406 100644 --- a/homeassistant/components/brottsplatskartan/manifest.json +++ b/homeassistant/components/brottsplatskartan/manifest.json @@ -1,7 +1,7 @@ { "domain": "brottsplatskartan", "name": "Brottsplatskartan", - "documentation": "https://www.home-assistant.io/components/brottsplatskartan", + "documentation": "https://www.home-assistant.io/integrations/brottsplatskartan", "requirements": [ "brottsplatskartan==0.0.1" ], diff --git a/homeassistant/components/browser/manifest.json b/homeassistant/components/browser/manifest.json index 61823564fe9180..2905bfcfe96138 100644 --- a/homeassistant/components/browser/manifest.json +++ b/homeassistant/components/browser/manifest.json @@ -1,7 +1,7 @@ { "domain": "browser", "name": "Browser", - "documentation": "https://www.home-assistant.io/components/browser", + "documentation": "https://www.home-assistant.io/integrations/browser", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index a47e3f69d5cf08..6ee4344b946fac 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -1,7 +1,7 @@ { "domain": "brunt", "name": "Brunt", - "documentation": "https://www.home-assistant.io/components/brunt", + "documentation": "https://www.home-assistant.io/integrations/brunt", "requirements": [ "brunt==0.1.3" ], diff --git a/homeassistant/components/bt_home_hub_5/manifest.json b/homeassistant/components/bt_home_hub_5/manifest.json index 927d9ea941230b..bee9cefce17fd1 100644 --- a/homeassistant/components/bt_home_hub_5/manifest.json +++ b/homeassistant/components/bt_home_hub_5/manifest.json @@ -1,7 +1,7 @@ { "domain": "bt_home_hub_5", "name": "Bt home hub 5", - "documentation": "https://www.home-assistant.io/components/bt_home_hub_5", + "documentation": "https://www.home-assistant.io/integrations/bt_home_hub_5", "requirements": [ "bthomehub5-devicelist==0.1.1" ], diff --git a/homeassistant/components/bt_smarthub/manifest.json b/homeassistant/components/bt_smarthub/manifest.json index 725541082e701d..985f3012422d91 100644 --- a/homeassistant/components/bt_smarthub/manifest.json +++ b/homeassistant/components/bt_smarthub/manifest.json @@ -1,7 +1,7 @@ { "domain": "bt_smarthub", "name": "Bt smarthub", - "documentation": "https://www.home-assistant.io/components/bt_smarthub", + "documentation": "https://www.home-assistant.io/integrations/bt_smarthub", "requirements": [ "btsmarthub_devicelist==0.1.3" ], diff --git a/homeassistant/components/buienradar/manifest.json b/homeassistant/components/buienradar/manifest.json index d25a2526a5dc37..cc3c3b02981596 100644 --- a/homeassistant/components/buienradar/manifest.json +++ b/homeassistant/components/buienradar/manifest.json @@ -1,7 +1,7 @@ { "domain": "buienradar", "name": "Buienradar", - "documentation": "https://www.home-assistant.io/components/buienradar", + "documentation": "https://www.home-assistant.io/integrations/buienradar", "requirements": [ "buienradar==1.0.1" ], diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 55cd555d989554..896ace7ba6a686 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -1,7 +1,7 @@ { "domain": "caldav", "name": "Caldav", - "documentation": "https://www.home-assistant.io/components/caldav", + "documentation": "https://www.home-assistant.io/integrations/caldav", "requirements": [ "caldav==0.6.1" ], diff --git a/homeassistant/components/calendar/manifest.json b/homeassistant/components/calendar/manifest.json index 3a09cd090a523d..3e6ee8422b4cd3 100644 --- a/homeassistant/components/calendar/manifest.json +++ b/homeassistant/components/calendar/manifest.json @@ -1,7 +1,7 @@ { "domain": "calendar", "name": "Calendar", - "documentation": "https://www.home-assistant.io/components/calendar", + "documentation": "https://www.home-assistant.io/integrations/calendar", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index 3af6a15ca5249f..a3395965e4f749 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -1,7 +1,7 @@ { "domain": "camera", "name": "Camera", - "documentation": "https://www.home-assistant.io/components/camera", + "documentation": "https://www.home-assistant.io/integrations/camera", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index 346c1c99f6df6b..f76d521853d231 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -1,7 +1,7 @@ { "domain": "canary", "name": "Canary", - "documentation": "https://www.home-assistant.io/components/canary", + "documentation": "https://www.home-assistant.io/integrations/canary", "requirements": [ "py-canary==0.5.0" ], diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 84a6a6e2934fd6..b6776a17f7cba0 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -2,7 +2,7 @@ "domain": "cast", "name": "Cast", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/cast", + "documentation": "https://www.home-assistant.io/integrations/cast", "requirements": ["pychromecast==4.0.1"], "dependencies": [], "zeroconf": ["_googlecast._tcp.local."], diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json index 781f27afb5f115..97f72f2ad1194a 100644 --- a/homeassistant/components/cert_expiry/manifest.json +++ b/homeassistant/components/cert_expiry/manifest.json @@ -1,7 +1,7 @@ { "domain": "cert_expiry", "name": "Cert expiry", - "documentation": "https://www.home-assistant.io/components/cert_expiry", + "documentation": "https://www.home-assistant.io/integrations/cert_expiry", "requirements": [], "config_flow": true, "dependencies": [], diff --git a/homeassistant/components/channels/manifest.json b/homeassistant/components/channels/manifest.json index 152c7d3a2dc5eb..c6ef7f8f127e11 100644 --- a/homeassistant/components/channels/manifest.json +++ b/homeassistant/components/channels/manifest.json @@ -1,7 +1,7 @@ { "domain": "channels", "name": "Channels", - "documentation": "https://www.home-assistant.io/components/channels", + "documentation": "https://www.home-assistant.io/integrations/channels", "requirements": [ "pychannels==1.0.0" ], diff --git a/homeassistant/components/cisco_ios/manifest.json b/homeassistant/components/cisco_ios/manifest.json index 9a12ba252e374a..4a04ffa32e6962 100644 --- a/homeassistant/components/cisco_ios/manifest.json +++ b/homeassistant/components/cisco_ios/manifest.json @@ -1,7 +1,7 @@ { "domain": "cisco_ios", "name": "Cisco ios", - "documentation": "https://www.home-assistant.io/components/cisco_ios", + "documentation": "https://www.home-assistant.io/integrations/cisco_ios", "requirements": [ "pexpect==4.6.0" ], diff --git a/homeassistant/components/cisco_mobility_express/manifest.json b/homeassistant/components/cisco_mobility_express/manifest.json index abdd2400311f24..b4bc2ff86d950a 100644 --- a/homeassistant/components/cisco_mobility_express/manifest.json +++ b/homeassistant/components/cisco_mobility_express/manifest.json @@ -1,7 +1,7 @@ { "domain": "cisco_mobility_express", "name": "Cisco mobility express", - "documentation": "https://www.home-assistant.io/components/cisco_mobility_express", + "documentation": "https://www.home-assistant.io/integrations/cisco_mobility_express", "requirements": [ "ciscomobilityexpress==0.3.3" ], diff --git a/homeassistant/components/cisco_webex_teams/manifest.json b/homeassistant/components/cisco_webex_teams/manifest.json index 21c4efe071c95c..3560b1e7fcd318 100644 --- a/homeassistant/components/cisco_webex_teams/manifest.json +++ b/homeassistant/components/cisco_webex_teams/manifest.json @@ -1,7 +1,7 @@ { "domain": "cisco_webex_teams", "name": "Cisco webex teams", - "documentation": "https://www.home-assistant.io/components/cisco_webex_teams", + "documentation": "https://www.home-assistant.io/integrations/cisco_webex_teams", "requirements": [ "webexteamssdk==1.1.1" ], diff --git a/homeassistant/components/ciscospark/manifest.json b/homeassistant/components/ciscospark/manifest.json index 926925a7bf19ec..8058088bf8ac6f 100644 --- a/homeassistant/components/ciscospark/manifest.json +++ b/homeassistant/components/ciscospark/manifest.json @@ -1,7 +1,7 @@ { "domain": "ciscospark", "name": "Ciscospark", - "documentation": "https://www.home-assistant.io/components/ciscospark", + "documentation": "https://www.home-assistant.io/integrations/ciscospark", "requirements": [ "ciscosparkapi==0.4.2" ], diff --git a/homeassistant/components/citybikes/manifest.json b/homeassistant/components/citybikes/manifest.json index ea1ceaa9531a54..f46c7ba708f688 100644 --- a/homeassistant/components/citybikes/manifest.json +++ b/homeassistant/components/citybikes/manifest.json @@ -1,7 +1,7 @@ { "domain": "citybikes", "name": "Citybikes", - "documentation": "https://www.home-assistant.io/components/citybikes", + "documentation": "https://www.home-assistant.io/integrations/citybikes", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/clementine/manifest.json b/homeassistant/components/clementine/manifest.json index 4d835ed4e7c2d1..35368dd6cdfa00 100644 --- a/homeassistant/components/clementine/manifest.json +++ b/homeassistant/components/clementine/manifest.json @@ -1,7 +1,7 @@ { "domain": "clementine", "name": "Clementine", - "documentation": "https://www.home-assistant.io/components/clementine", + "documentation": "https://www.home-assistant.io/integrations/clementine", "requirements": [ "python-clementine-remote==1.0.1" ], diff --git a/homeassistant/components/clickatell/manifest.json b/homeassistant/components/clickatell/manifest.json index ffd550eebee871..a10da6e1cc0ca0 100644 --- a/homeassistant/components/clickatell/manifest.json +++ b/homeassistant/components/clickatell/manifest.json @@ -1,7 +1,7 @@ { "domain": "clickatell", "name": "Clickatell", - "documentation": "https://www.home-assistant.io/components/clickatell", + "documentation": "https://www.home-assistant.io/integrations/clickatell", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/clicksend/manifest.json b/homeassistant/components/clicksend/manifest.json index 3831982509431e..6a28b3c30ca415 100644 --- a/homeassistant/components/clicksend/manifest.json +++ b/homeassistant/components/clicksend/manifest.json @@ -1,7 +1,7 @@ { "domain": "clicksend", "name": "Clicksend", - "documentation": "https://www.home-assistant.io/components/clicksend", + "documentation": "https://www.home-assistant.io/integrations/clicksend", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/clicksend_tts/manifest.json b/homeassistant/components/clicksend_tts/manifest.json index c2a86f426e43e0..8aa3eacf405f21 100644 --- a/homeassistant/components/clicksend_tts/manifest.json +++ b/homeassistant/components/clicksend_tts/manifest.json @@ -1,7 +1,7 @@ { "domain": "clicksend_tts", "name": "Clicksend tts", - "documentation": "https://www.home-assistant.io/components/clicksend_tts", + "documentation": "https://www.home-assistant.io/integrations/clicksend_tts", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/climate/manifest.json b/homeassistant/components/climate/manifest.json index ca5312e7670b1c..5933eaf9071e23 100644 --- a/homeassistant/components/climate/manifest.json +++ b/homeassistant/components/climate/manifest.json @@ -1,7 +1,7 @@ { "domain": "climate", "name": "Climate", - "documentation": "https://www.home-assistant.io/components/climate", + "documentation": "https://www.home-assistant.io/integrations/climate", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 3daeac43da942c..b15fa32cb1378c 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "cloud", "name": "Cloud", - "documentation": "https://www.home-assistant.io/components/cloud", + "documentation": "https://www.home-assistant.io/integrations/cloud", "requirements": ["hass-nabucasa==0.17"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/components/cloudflare/manifest.json b/homeassistant/components/cloudflare/manifest.json index 7716ae65c4e1a1..78bc6de99c8187 100644 --- a/homeassistant/components/cloudflare/manifest.json +++ b/homeassistant/components/cloudflare/manifest.json @@ -1,7 +1,7 @@ { "domain": "cloudflare", "name": "Cloudflare", - "documentation": "https://www.home-assistant.io/components/cloudflare", + "documentation": "https://www.home-assistant.io/integrations/cloudflare", "requirements": [ "pycfdns==0.0.1" ], diff --git a/homeassistant/components/cmus/manifest.json b/homeassistant/components/cmus/manifest.json index 1528f4252b1098..fe5b8e155c266f 100644 --- a/homeassistant/components/cmus/manifest.json +++ b/homeassistant/components/cmus/manifest.json @@ -1,7 +1,7 @@ { "domain": "cmus", "name": "Cmus", - "documentation": "https://www.home-assistant.io/components/cmus", + "documentation": "https://www.home-assistant.io/integrations/cmus", "requirements": [ "pycmus==0.1.1" ], diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index ac42e374fdd23e..f07813b3db5a14 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -1,7 +1,7 @@ { "domain": "co2signal", "name": "Co2signal", - "documentation": "https://www.home-assistant.io/components/co2signal", + "documentation": "https://www.home-assistant.io/integrations/co2signal", "requirements": [ "co2signal==0.4.2" ], diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index 5f8a189c7d129f..2da323f08158fc 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -1,7 +1,7 @@ { "domain": "coinbase", "name": "Coinbase", - "documentation": "https://www.home-assistant.io/components/coinbase", + "documentation": "https://www.home-assistant.io/integrations/coinbase", "requirements": [ "coinbase==2.1.0" ], diff --git a/homeassistant/components/coinmarketcap/manifest.json b/homeassistant/components/coinmarketcap/manifest.json index 0afb1b1c28f317..ec9aec6c65449e 100644 --- a/homeassistant/components/coinmarketcap/manifest.json +++ b/homeassistant/components/coinmarketcap/manifest.json @@ -1,7 +1,7 @@ { "domain": "coinmarketcap", "name": "Coinmarketcap", - "documentation": "https://www.home-assistant.io/components/coinmarketcap", + "documentation": "https://www.home-assistant.io/integrations/coinmarketcap", "requirements": [ "coinmarketcap==5.0.3" ], diff --git a/homeassistant/components/comed_hourly_pricing/manifest.json b/homeassistant/components/comed_hourly_pricing/manifest.json index 47c7931a0e93d2..89fbb84e82d30a 100644 --- a/homeassistant/components/comed_hourly_pricing/manifest.json +++ b/homeassistant/components/comed_hourly_pricing/manifest.json @@ -1,7 +1,7 @@ { "domain": "comed_hourly_pricing", "name": "Comed hourly pricing", - "documentation": "https://www.home-assistant.io/components/comed_hourly_pricing", + "documentation": "https://www.home-assistant.io/integrations/comed_hourly_pricing", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/comfoconnect/manifest.json b/homeassistant/components/comfoconnect/manifest.json index 03319aeffa89b3..57daba7fdbd1dd 100644 --- a/homeassistant/components/comfoconnect/manifest.json +++ b/homeassistant/components/comfoconnect/manifest.json @@ -1,7 +1,7 @@ { "domain": "comfoconnect", "name": "Comfoconnect", - "documentation": "https://www.home-assistant.io/components/comfoconnect", + "documentation": "https://www.home-assistant.io/integrations/comfoconnect", "requirements": [ "pycomfoconnect==0.3" ], diff --git a/homeassistant/components/command_line/manifest.json b/homeassistant/components/command_line/manifest.json index ff94522210d818..4d7dfc8994ffec 100644 --- a/homeassistant/components/command_line/manifest.json +++ b/homeassistant/components/command_line/manifest.json @@ -1,7 +1,7 @@ { "domain": "command_line", "name": "Command line", - "documentation": "https://www.home-assistant.io/components/command_line", + "documentation": "https://www.home-assistant.io/integrations/command_line", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/concord232/manifest.json b/homeassistant/components/concord232/manifest.json index f26da49d3f1b84..f5ff021b6d1766 100644 --- a/homeassistant/components/concord232/manifest.json +++ b/homeassistant/components/concord232/manifest.json @@ -1,7 +1,7 @@ { "domain": "concord232", "name": "Concord232", - "documentation": "https://www.home-assistant.io/components/concord232", + "documentation": "https://www.home-assistant.io/integrations/concord232", "requirements": [ "concord232==0.15" ], diff --git a/homeassistant/components/config/manifest.json b/homeassistant/components/config/manifest.json index 9c0c50a25957ee..c6e99b43c494c8 100644 --- a/homeassistant/components/config/manifest.json +++ b/homeassistant/components/config/manifest.json @@ -1,7 +1,7 @@ { "domain": "config", "name": "Config", - "documentation": "https://www.home-assistant.io/components/config", + "documentation": "https://www.home-assistant.io/integrations/config", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/configurator/manifest.json b/homeassistant/components/configurator/manifest.json index f01fe7324fa493..10c067d4a22525 100644 --- a/homeassistant/components/configurator/manifest.json +++ b/homeassistant/components/configurator/manifest.json @@ -1,7 +1,7 @@ { "domain": "configurator", "name": "Configurator", - "documentation": "https://www.home-assistant.io/components/configurator", + "documentation": "https://www.home-assistant.io/integrations/configurator", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ddd3b6205efdd4..0d6d67cf254d51 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -1,7 +1,7 @@ { "domain": "conversation", "name": "Conversation", - "documentation": "https://www.home-assistant.io/components/conversation", + "documentation": "https://www.home-assistant.io/integrations/conversation", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 9489dc72689e5b..69ab8ee3c4b199 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -1,7 +1,7 @@ { "domain": "coolmaster", "name": "Coolmaster", - "documentation": "https://www.home-assistant.io/components/coolmaster", + "documentation": "https://www.home-assistant.io/integrations/coolmaster", "requirements": [ "pycoolmasternet==0.0.4" ], diff --git a/homeassistant/components/counter/manifest.json b/homeassistant/components/counter/manifest.json index ae7066ea82d28d..3fd533054d8eb2 100644 --- a/homeassistant/components/counter/manifest.json +++ b/homeassistant/components/counter/manifest.json @@ -1,7 +1,7 @@ { "domain": "counter", "name": "Counter", - "documentation": "https://www.home-assistant.io/components/counter", + "documentation": "https://www.home-assistant.io/integrations/counter", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/cover/manifest.json b/homeassistant/components/cover/manifest.json index da5a644334cb5e..1d82dcb5b0c7a4 100644 --- a/homeassistant/components/cover/manifest.json +++ b/homeassistant/components/cover/manifest.json @@ -1,7 +1,7 @@ { "domain": "cover", "name": "Cover", - "documentation": "https://www.home-assistant.io/components/cover", + "documentation": "https://www.home-assistant.io/integrations/cover", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/cppm_tracker/manifest.json b/homeassistant/components/cppm_tracker/manifest.json index 5a1bdbf5a452ec..b2abc40dca23f2 100644 --- a/homeassistant/components/cppm_tracker/manifest.json +++ b/homeassistant/components/cppm_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "cppm_tracker", "name": "Cppm tracker", - "documentation": "https://www.home-assistant.io/components/cppm_tracker", + "documentation": "https://www.home-assistant.io/integrations/cppm_tracker", "requirements": [ "clearpasspy==1.0.2" ], diff --git a/homeassistant/components/cpuspeed/manifest.json b/homeassistant/components/cpuspeed/manifest.json index 9034cb7740d0f3..7950cad9b8bbc8 100644 --- a/homeassistant/components/cpuspeed/manifest.json +++ b/homeassistant/components/cpuspeed/manifest.json @@ -1,7 +1,7 @@ { "domain": "cpuspeed", "name": "Cpuspeed", - "documentation": "https://www.home-assistant.io/components/cpuspeed", + "documentation": "https://www.home-assistant.io/integrations/cpuspeed", "requirements": [ "py-cpuinfo==5.0.0" ], diff --git a/homeassistant/components/crimereports/manifest.json b/homeassistant/components/crimereports/manifest.json index 0f74216b9b2156..c5cc45d3192542 100644 --- a/homeassistant/components/crimereports/manifest.json +++ b/homeassistant/components/crimereports/manifest.json @@ -1,7 +1,7 @@ { "domain": "crimereports", "name": "Crimereports", - "documentation": "https://www.home-assistant.io/components/crimereports", + "documentation": "https://www.home-assistant.io/integrations/crimereports", "requirements": [ "crimereports==1.0.1" ], diff --git a/homeassistant/components/cups/manifest.json b/homeassistant/components/cups/manifest.json index def2846c4ca3b6..e30d64510ff4ec 100644 --- a/homeassistant/components/cups/manifest.json +++ b/homeassistant/components/cups/manifest.json @@ -1,7 +1,7 @@ { "domain": "cups", "name": "Cups", - "documentation": "https://www.home-assistant.io/components/cups", + "documentation": "https://www.home-assistant.io/integrations/cups", "requirements": [ "pycups==1.9.73" ], diff --git a/homeassistant/components/currencylayer/manifest.json b/homeassistant/components/currencylayer/manifest.json index 7064590bf25877..2db35dead60bfb 100644 --- a/homeassistant/components/currencylayer/manifest.json +++ b/homeassistant/components/currencylayer/manifest.json @@ -1,7 +1,7 @@ { "domain": "currencylayer", "name": "Currencylayer", - "documentation": "https://www.home-assistant.io/components/currencylayer", + "documentation": "https://www.home-assistant.io/integrations/currencylayer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index a60355efa0bac7..f81cb1715807c0 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -2,7 +2,7 @@ "domain": "daikin", "name": "Daikin", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/daikin", + "documentation": "https://www.home-assistant.io/integrations/daikin", "requirements": [ "pydaikin==1.6.1" ], diff --git a/homeassistant/components/danfoss_air/manifest.json b/homeassistant/components/danfoss_air/manifest.json index a210b5a78d1d50..189b685d4ce480 100644 --- a/homeassistant/components/danfoss_air/manifest.json +++ b/homeassistant/components/danfoss_air/manifest.json @@ -1,7 +1,7 @@ { "domain": "danfoss_air", "name": "Danfoss air", - "documentation": "https://www.home-assistant.io/components/danfoss_air", + "documentation": "https://www.home-assistant.io/integrations/danfoss_air", "requirements": [ "pydanfossair==0.1.0" ], diff --git a/homeassistant/components/darksky/manifest.json b/homeassistant/components/darksky/manifest.json index e4e6482484cd21..0046e51463e2f8 100644 --- a/homeassistant/components/darksky/manifest.json +++ b/homeassistant/components/darksky/manifest.json @@ -1,7 +1,7 @@ { "domain": "darksky", "name": "Darksky", - "documentation": "https://www.home-assistant.io/components/darksky", + "documentation": "https://www.home-assistant.io/integrations/darksky", "requirements": [ "python-forecastio==1.4.0" ], diff --git a/homeassistant/components/datadog/manifest.json b/homeassistant/components/datadog/manifest.json index 40a2e82d53ac3e..36de2ff2101b24 100644 --- a/homeassistant/components/datadog/manifest.json +++ b/homeassistant/components/datadog/manifest.json @@ -1,7 +1,7 @@ { "domain": "datadog", "name": "Datadog", - "documentation": "https://www.home-assistant.io/components/datadog", + "documentation": "https://www.home-assistant.io/integrations/datadog", "requirements": [ "datadog==0.15.0" ], diff --git a/homeassistant/components/ddwrt/manifest.json b/homeassistant/components/ddwrt/manifest.json index 3c877a34841478..874b24e370b540 100644 --- a/homeassistant/components/ddwrt/manifest.json +++ b/homeassistant/components/ddwrt/manifest.json @@ -1,7 +1,7 @@ { "domain": "ddwrt", "name": "Ddwrt", - "documentation": "https://www.home-assistant.io/components/ddwrt", + "documentation": "https://www.home-assistant.io/integrations/ddwrt", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 4aec29008dee80..1e5cd4144253d2 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -2,7 +2,7 @@ "domain": "deconz", "name": "Deconz", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/deconz", + "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ "pydeconz==63" ], diff --git a/homeassistant/components/decora/manifest.json b/homeassistant/components/decora/manifest.json index 923a543e82788b..5142b5fb2e2cd6 100644 --- a/homeassistant/components/decora/manifest.json +++ b/homeassistant/components/decora/manifest.json @@ -1,7 +1,7 @@ { "domain": "decora", "name": "Decora", - "documentation": "https://www.home-assistant.io/components/decora", + "documentation": "https://www.home-assistant.io/integrations/decora", "requirements": [ "bluepy==1.1.4", "decora==0.6" diff --git a/homeassistant/components/decora_wifi/manifest.json b/homeassistant/components/decora_wifi/manifest.json index 42ab6bfd6c1668..14b8829fae8671 100644 --- a/homeassistant/components/decora_wifi/manifest.json +++ b/homeassistant/components/decora_wifi/manifest.json @@ -1,7 +1,7 @@ { "domain": "decora_wifi", "name": "Decora wifi", - "documentation": "https://www.home-assistant.io/components/decora_wifi", + "documentation": "https://www.home-assistant.io/integrations/decora_wifi", "requirements": [ "decora_wifi==1.4" ], diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 6969d9bba7e9a4..a240599c42029c 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -1,7 +1,7 @@ { "domain": "default_config", "name": "Default config", - "documentation": "https://www.home-assistant.io/components/default_config", + "documentation": "https://www.home-assistant.io/integrations/default_config", "requirements": [], "dependencies": [ "automation", diff --git a/homeassistant/components/delijn/manifest.json b/homeassistant/components/delijn/manifest.json index 90e1a4e3b151c6..2d550a0851f839 100644 --- a/homeassistant/components/delijn/manifest.json +++ b/homeassistant/components/delijn/manifest.json @@ -1,7 +1,7 @@ { "domain": "delijn", "name": "De Lijn", - "documentation": "https://www.home-assistant.io/components/delijn", + "documentation": "https://www.home-assistant.io/integrations/delijn", "dependencies": [], "codeowners": ["@bollewolle"], "requirements": ["pydelijn==0.5.1"] diff --git a/homeassistant/components/deluge/manifest.json b/homeassistant/components/deluge/manifest.json index d33a140cedb914..b2eb3ada65f0b3 100644 --- a/homeassistant/components/deluge/manifest.json +++ b/homeassistant/components/deluge/manifest.json @@ -1,7 +1,7 @@ { "domain": "deluge", "name": "Deluge", - "documentation": "https://www.home-assistant.io/components/deluge", + "documentation": "https://www.home-assistant.io/integrations/deluge", "requirements": [ "deluge-client==1.7.1" ], diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index 4f167ecae25afa..a4802c3b67b968 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -1,7 +1,7 @@ { "domain": "demo", "name": "Demo", - "documentation": "https://www.home-assistant.io/components/demo", + "documentation": "https://www.home-assistant.io/integrations/demo", "requirements": [], "dependencies": [ "conversation", diff --git a/homeassistant/components/denon/manifest.json b/homeassistant/components/denon/manifest.json index 2068b72fa9d494..92b2aebab40f5f 100644 --- a/homeassistant/components/denon/manifest.json +++ b/homeassistant/components/denon/manifest.json @@ -1,7 +1,7 @@ { "domain": "denon", "name": "Denon", - "documentation": "https://www.home-assistant.io/components/denon", + "documentation": "https://www.home-assistant.io/integrations/denon", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 34699d666ad02d..9e084c78e21c92 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -1,7 +1,7 @@ { "domain": "denonavr", "name": "Denonavr", - "documentation": "https://www.home-assistant.io/components/denonavr", + "documentation": "https://www.home-assistant.io/integrations/denonavr", "requirements": [ "denonavr==0.7.10" ], diff --git a/homeassistant/components/deutsche_bahn/manifest.json b/homeassistant/components/deutsche_bahn/manifest.json index 463c7d03fbb236..9a2bf66601617b 100644 --- a/homeassistant/components/deutsche_bahn/manifest.json +++ b/homeassistant/components/deutsche_bahn/manifest.json @@ -1,7 +1,7 @@ { "domain": "deutsche_bahn", "name": "Deutsche bahn", - "documentation": "https://www.home-assistant.io/components/deutsche_bahn", + "documentation": "https://www.home-assistant.io/integrations/deutsche_bahn", "requirements": [ "schiene==0.23" ], diff --git a/homeassistant/components/device_automation/manifest.json b/homeassistant/components/device_automation/manifest.json index a95e9c4f68fbb1..50b1f9d357a85f 100644 --- a/homeassistant/components/device_automation/manifest.json +++ b/homeassistant/components/device_automation/manifest.json @@ -1,7 +1,7 @@ { "domain": "device_automation", "name": "Device automation", - "documentation": "https://www.home-assistant.io/components/device_automation", + "documentation": "https://www.home-assistant.io/integrations/device_automation", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/device_sun_light_trigger/manifest.json b/homeassistant/components/device_sun_light_trigger/manifest.json index 40ab85bc1e5fbb..3bea097b831f26 100644 --- a/homeassistant/components/device_sun_light_trigger/manifest.json +++ b/homeassistant/components/device_sun_light_trigger/manifest.json @@ -1,7 +1,7 @@ { "domain": "device_sun_light_trigger", "name": "Device sun light trigger", - "documentation": "https://www.home-assistant.io/components/device_sun_light_trigger", + "documentation": "https://www.home-assistant.io/integrations/device_sun_light_trigger", "requirements": [], "dependencies": [ "device_tracker", diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 7b32f7845a6d5b..0e1e0e8cd81a88 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "device_tracker", "name": "Device tracker", - "documentation": "https://www.home-assistant.io/components/device_tracker", + "documentation": "https://www.home-assistant.io/integrations/device_tracker", "requirements": [], "dependencies": [ "group", diff --git a/homeassistant/components/dht/manifest.json b/homeassistant/components/dht/manifest.json index 05889bdd326106..e1d9273b7975a1 100644 --- a/homeassistant/components/dht/manifest.json +++ b/homeassistant/components/dht/manifest.json @@ -1,7 +1,7 @@ { "domain": "dht", "name": "Dht", - "documentation": "https://www.home-assistant.io/components/dht", + "documentation": "https://www.home-assistant.io/integrations/dht", "requirements": [ "Adafruit-DHT==1.4.0" ], diff --git a/homeassistant/components/dialogflow/manifest.json b/homeassistant/components/dialogflow/manifest.json index aa8b584aecaa8f..d9e821c82fd6af 100644 --- a/homeassistant/components/dialogflow/manifest.json +++ b/homeassistant/components/dialogflow/manifest.json @@ -2,7 +2,7 @@ "domain": "dialogflow", "name": "Dialogflow", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/dialogflow", + "documentation": "https://www.home-assistant.io/integrations/dialogflow", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/digital_ocean/manifest.json b/homeassistant/components/digital_ocean/manifest.json index 2ef940f60bd963..eb19a5c3a85945 100644 --- a/homeassistant/components/digital_ocean/manifest.json +++ b/homeassistant/components/digital_ocean/manifest.json @@ -1,7 +1,7 @@ { "domain": "digital_ocean", "name": "Digital ocean", - "documentation": "https://www.home-assistant.io/components/digital_ocean", + "documentation": "https://www.home-assistant.io/integrations/digital_ocean", "requirements": [ "python-digitalocean==1.13.2" ], diff --git a/homeassistant/components/digitalloggers/manifest.json b/homeassistant/components/digitalloggers/manifest.json index 990b39b21a5fdb..4c58e090a95d71 100644 --- a/homeassistant/components/digitalloggers/manifest.json +++ b/homeassistant/components/digitalloggers/manifest.json @@ -1,7 +1,7 @@ { "domain": "digitalloggers", "name": "Digitalloggers", - "documentation": "https://www.home-assistant.io/components/digitalloggers", + "documentation": "https://www.home-assistant.io/integrations/digitalloggers", "requirements": [ "dlipower==0.7.165" ], diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index 7dbe6122ac1e34..8b65d7a680bdbe 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -1,7 +1,7 @@ { "domain": "directv", "name": "Directv", - "documentation": "https://www.home-assistant.io/components/directv", + "documentation": "https://www.home-assistant.io/integrations/directv", "requirements": [ "directpy==0.5" ], diff --git a/homeassistant/components/discogs/manifest.json b/homeassistant/components/discogs/manifest.json index ca304bce88bcff..18282f077817a6 100644 --- a/homeassistant/components/discogs/manifest.json +++ b/homeassistant/components/discogs/manifest.json @@ -1,7 +1,7 @@ { "domain": "discogs", "name": "Discogs", - "documentation": "https://www.home-assistant.io/components/discogs", + "documentation": "https://www.home-assistant.io/integrations/discogs", "requirements": [ "discogs_client==2.2.1" ], diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 31940c28c4ef7a..50c03bad25d18a 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -1,7 +1,7 @@ { "domain": "discord", "name": "Discord", - "documentation": "https://www.home-assistant.io/components/discord", + "documentation": "https://www.home-assistant.io/integrations/discord", "requirements": [ "discord.py==1.2.3" ], diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 845e1af15d4053..56d968189cf917 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -1,7 +1,7 @@ { "domain": "discovery", "name": "Discovery", - "documentation": "https://www.home-assistant.io/components/discovery", + "documentation": "https://www.home-assistant.io/integrations/discovery", "requirements": [ "netdisco==2.6.0" ], diff --git a/homeassistant/components/dlib_face_detect/manifest.json b/homeassistant/components/dlib_face_detect/manifest.json index c2ede62ee5b015..431afc080f442a 100644 --- a/homeassistant/components/dlib_face_detect/manifest.json +++ b/homeassistant/components/dlib_face_detect/manifest.json @@ -1,7 +1,7 @@ { "domain": "dlib_face_detect", "name": "Dlib face detect", - "documentation": "https://www.home-assistant.io/components/dlib_face_detect", + "documentation": "https://www.home-assistant.io/integrations/dlib_face_detect", "requirements": [ "face_recognition==1.2.3" ], diff --git a/homeassistant/components/dlib_face_identify/manifest.json b/homeassistant/components/dlib_face_identify/manifest.json index 388017f78bb45a..2f764ae2817f5f 100644 --- a/homeassistant/components/dlib_face_identify/manifest.json +++ b/homeassistant/components/dlib_face_identify/manifest.json @@ -1,7 +1,7 @@ { "domain": "dlib_face_identify", "name": "Dlib face identify", - "documentation": "https://www.home-assistant.io/components/dlib_face_identify", + "documentation": "https://www.home-assistant.io/integrations/dlib_face_identify", "requirements": [ "face_recognition==1.2.3" ], diff --git a/homeassistant/components/dlink/manifest.json b/homeassistant/components/dlink/manifest.json index 8f7d07eb0db39b..b0b8c60121aa7a 100644 --- a/homeassistant/components/dlink/manifest.json +++ b/homeassistant/components/dlink/manifest.json @@ -1,7 +1,7 @@ { "domain": "dlink", "name": "Dlink", - "documentation": "https://www.home-assistant.io/components/dlink", + "documentation": "https://www.home-assistant.io/integrations/dlink", "requirements": [ "pyW215==0.6.0" ], diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index bf05d5c7f63fdd..008a1293e41c95 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -1,7 +1,7 @@ { "domain": "dlna_dmr", "name": "Dlna dmr", - "documentation": "https://www.home-assistant.io/components/dlna_dmr", + "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "requirements": [ "async-upnp-client==0.14.11" ], diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index 544ac9b0fbafa5..4f3d84da476cac 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -1,7 +1,7 @@ { "domain": "dnsip", "name": "Dnsip", - "documentation": "https://www.home-assistant.io/components/dnsip", + "documentation": "https://www.home-assistant.io/integrations/dnsip", "requirements": [ "aiodns==2.0.0" ], diff --git a/homeassistant/components/dominos/manifest.json b/homeassistant/components/dominos/manifest.json index f8d13b49f93327..933af93a77aa7c 100644 --- a/homeassistant/components/dominos/manifest.json +++ b/homeassistant/components/dominos/manifest.json @@ -1,7 +1,7 @@ { "domain": "dominos", "name": "Dominos", - "documentation": "https://www.home-assistant.io/components/dominos", + "documentation": "https://www.home-assistant.io/integrations/dominos", "requirements": [ "pizzapi==0.0.3" ], diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 75c1bd3dcd3814..e0dcb48527f91b 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -1,7 +1,7 @@ { "domain": "doods", "name": "DOODS - Distributed Outside Object Detection Service", - "documentation": "https://www.home-assistant.io/components/doods", + "documentation": "https://www.home-assistant.io/integrations/doods", "requirements": [ "pydoods==1.0.2" ], diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 3fb9fdc753b7d5..c9cdb32e18af31 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -1,7 +1,7 @@ { "domain": "doorbird", "name": "Doorbird", - "documentation": "https://www.home-assistant.io/components/doorbird", + "documentation": "https://www.home-assistant.io/integrations/doorbird", "requirements": [ "doorbirdpy==2.0.8" ], diff --git a/homeassistant/components/dovado/manifest.json b/homeassistant/components/dovado/manifest.json index 122d774c268221..ac0044c7a89127 100644 --- a/homeassistant/components/dovado/manifest.json +++ b/homeassistant/components/dovado/manifest.json @@ -1,7 +1,7 @@ { "domain": "dovado", "name": "Dovado", - "documentation": "https://www.home-assistant.io/components/dovado", + "documentation": "https://www.home-assistant.io/integrations/dovado", "requirements": [ "dovado==0.4.1" ], diff --git a/homeassistant/components/downloader/manifest.json b/homeassistant/components/downloader/manifest.json index 514509c004d508..241dadddf4ed7c 100644 --- a/homeassistant/components/downloader/manifest.json +++ b/homeassistant/components/downloader/manifest.json @@ -1,7 +1,7 @@ { "domain": "downloader", "name": "Downloader", - "documentation": "https://www.home-assistant.io/components/downloader", + "documentation": "https://www.home-assistant.io/integrations/downloader", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index 21c98d56d1d55a..250adab263b09f 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -1,7 +1,7 @@ { "domain": "dsmr", "name": "Dsmr", - "documentation": "https://www.home-assistant.io/components/dsmr", + "documentation": "https://www.home-assistant.io/integrations/dsmr", "requirements": [ "dsmr_parser==0.12" ], diff --git a/homeassistant/components/dte_energy_bridge/manifest.json b/homeassistant/components/dte_energy_bridge/manifest.json index fbf7a00f8e6b0a..f3ccbdeebb221e 100644 --- a/homeassistant/components/dte_energy_bridge/manifest.json +++ b/homeassistant/components/dte_energy_bridge/manifest.json @@ -1,7 +1,7 @@ { "domain": "dte_energy_bridge", "name": "Dte energy bridge", - "documentation": "https://www.home-assistant.io/components/dte_energy_bridge", + "documentation": "https://www.home-assistant.io/integrations/dte_energy_bridge", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/dublin_bus_transport/manifest.json b/homeassistant/components/dublin_bus_transport/manifest.json index fc13fddc9364e6..3e377f3a2eaffa 100644 --- a/homeassistant/components/dublin_bus_transport/manifest.json +++ b/homeassistant/components/dublin_bus_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "dublin_bus_transport", "name": "Dublin bus transport", - "documentation": "https://www.home-assistant.io/components/dublin_bus_transport", + "documentation": "https://www.home-assistant.io/integrations/dublin_bus_transport", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/duckdns/manifest.json b/homeassistant/components/duckdns/manifest.json index ed38d35346f220..9e0ad6c001cae8 100644 --- a/homeassistant/components/duckdns/manifest.json +++ b/homeassistant/components/duckdns/manifest.json @@ -1,7 +1,7 @@ { "domain": "duckdns", "name": "Duckdns", - "documentation": "https://www.home-assistant.io/components/duckdns", + "documentation": "https://www.home-assistant.io/integrations/duckdns", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/duke_energy/manifest.json b/homeassistant/components/duke_energy/manifest.json index 602dfec801fd75..131063ad81fb42 100644 --- a/homeassistant/components/duke_energy/manifest.json +++ b/homeassistant/components/duke_energy/manifest.json @@ -1,7 +1,7 @@ { "domain": "duke_energy", "name": "Duke energy", - "documentation": "https://www.home-assistant.io/components/duke_energy", + "documentation": "https://www.home-assistant.io/integrations/duke_energy", "requirements": [ "pydukeenergy==0.0.6" ], diff --git a/homeassistant/components/dunehd/manifest.json b/homeassistant/components/dunehd/manifest.json index 35e6c4a2449ed9..47250b32cbc2a6 100644 --- a/homeassistant/components/dunehd/manifest.json +++ b/homeassistant/components/dunehd/manifest.json @@ -1,7 +1,7 @@ { "domain": "dunehd", "name": "Dunehd", - "documentation": "https://www.home-assistant.io/components/dunehd", + "documentation": "https://www.home-assistant.io/integrations/dunehd", "requirements": [ "pdunehd==1.3" ], diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index a2b21a9e0bf946..a35fcbcdf6834d 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -1,7 +1,7 @@ { "domain": "dwd_weather_warnings", "name": "Dwd weather warnings", - "documentation": "https://www.home-assistant.io/components/dwd_weather_warnings", + "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/dweet/manifest.json b/homeassistant/components/dweet/manifest.json index e0a00620210afe..75d8cfb6054c16 100644 --- a/homeassistant/components/dweet/manifest.json +++ b/homeassistant/components/dweet/manifest.json @@ -1,7 +1,7 @@ { "domain": "dweet", "name": "Dweet", - "documentation": "https://www.home-assistant.io/components/dweet", + "documentation": "https://www.home-assistant.io/integrations/dweet", "requirements": [ "dweepy==0.3.0" ], diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 7b956dd96c8324..92940c8c1e19fa 100644 --- a/homeassistant/components/dyson/manifest.json +++ b/homeassistant/components/dyson/manifest.json @@ -1,7 +1,7 @@ { "domain": "dyson", "name": "Dyson", - "documentation": "https://www.home-assistant.io/components/dyson", + "documentation": "https://www.home-assistant.io/integrations/dyson", "requirements": [ "libpurecool==0.5.0" ], diff --git a/homeassistant/components/ebox/manifest.json b/homeassistant/components/ebox/manifest.json index 16b033df8fdc09..d32206da72607c 100644 --- a/homeassistant/components/ebox/manifest.json +++ b/homeassistant/components/ebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "ebox", "name": "Ebox", - "documentation": "https://www.home-assistant.io/components/ebox", + "documentation": "https://www.home-assistant.io/integrations/ebox", "requirements": [ "pyebox==1.1.4" ], diff --git a/homeassistant/components/ebusd/manifest.json b/homeassistant/components/ebusd/manifest.json index 46b8fb761dcb7e..b9be062d3e2ad3 100644 --- a/homeassistant/components/ebusd/manifest.json +++ b/homeassistant/components/ebusd/manifest.json @@ -1,7 +1,7 @@ { "domain": "ebusd", "name": "Ebusd", - "documentation": "https://www.home-assistant.io/components/ebusd", + "documentation": "https://www.home-assistant.io/integrations/ebusd", "requirements": [ "ebusdpy==0.0.16" ], diff --git a/homeassistant/components/ecoal_boiler/manifest.json b/homeassistant/components/ecoal_boiler/manifest.json index 5bd488e0ff4bd1..c1bf938dc34044 100644 --- a/homeassistant/components/ecoal_boiler/manifest.json +++ b/homeassistant/components/ecoal_boiler/manifest.json @@ -1,7 +1,7 @@ { "domain": "ecoal_boiler", "name": "Ecoal boiler", - "documentation": "https://www.home-assistant.io/components/ecoal_boiler", + "documentation": "https://www.home-assistant.io/integrations/ecoal_boiler", "requirements": [ "ecoaliface==0.4.0" ], diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 148e355a3d9e9a..bc87b3cd64e654 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -2,7 +2,7 @@ "domain": "ecobee", "name": "Ecobee", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ecobee", + "documentation": "https://www.home-assistant.io/integrations/ecobee", "dependencies": [], "requirements": ["python-ecobee-api==0.1.4"], "codeowners": ["@marthoc"] diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 3ae6b1eac35559..7ce52c021a1ac6 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -1,7 +1,7 @@ { "domain": "econet", "name": "Econet", - "documentation": "https://www.home-assistant.io/components/econet", + "documentation": "https://www.home-assistant.io/integrations/econet", "requirements": [ "pyeconet==0.0.11" ], diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 4495cb3c2f9048..5de2390dd30fcc 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -1,7 +1,7 @@ { "domain": "ecovacs", "name": "Ecovacs", - "documentation": "https://www.home-assistant.io/components/ecovacs", + "documentation": "https://www.home-assistant.io/integrations/ecovacs", "requirements": [ "sucks==0.9.4" ], diff --git a/homeassistant/components/eddystone_temperature/manifest.json b/homeassistant/components/eddystone_temperature/manifest.json index 4684655aa372a8..36918fa5ee5f82 100644 --- a/homeassistant/components/eddystone_temperature/manifest.json +++ b/homeassistant/components/eddystone_temperature/manifest.json @@ -1,7 +1,7 @@ { "domain": "eddystone_temperature", "name": "Eddystone temperature", - "documentation": "https://www.home-assistant.io/components/eddystone_temperature", + "documentation": "https://www.home-assistant.io/integrations/eddystone_temperature", "requirements": [ "beacontools[scan]==1.2.3", "construct==2.9.45" diff --git a/homeassistant/components/edimax/manifest.json b/homeassistant/components/edimax/manifest.json index 9fe0e4c50c9693..6d1d444d2f4754 100644 --- a/homeassistant/components/edimax/manifest.json +++ b/homeassistant/components/edimax/manifest.json @@ -1,7 +1,7 @@ { "domain": "edimax", "name": "Edimax", - "documentation": "https://www.home-assistant.io/components/edimax", + "documentation": "https://www.home-assistant.io/integrations/edimax", "requirements": [ "pyedimax==0.1" ], diff --git a/homeassistant/components/ee_brightbox/manifest.json b/homeassistant/components/ee_brightbox/manifest.json index 967f04228a825e..d4ae0c9d6dfc14 100644 --- a/homeassistant/components/ee_brightbox/manifest.json +++ b/homeassistant/components/ee_brightbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "ee_brightbox", "name": "Ee brightbox", - "documentation": "https://www.home-assistant.io/components/ee_brightbox", + "documentation": "https://www.home-assistant.io/integrations/ee_brightbox", "requirements": [ "eebrightbox==0.0.4" ], diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index f4ca116a647084..99b966c6c501ac 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -1,7 +1,7 @@ { "domain": "efergy", "name": "Efergy", - "documentation": "https://www.home-assistant.io/components/efergy", + "documentation": "https://www.home-assistant.io/integrations/efergy", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/egardia/manifest.json b/homeassistant/components/egardia/manifest.json index 6f103449868ab1..b0dfc63d929b27 100644 --- a/homeassistant/components/egardia/manifest.json +++ b/homeassistant/components/egardia/manifest.json @@ -1,7 +1,7 @@ { "domain": "egardia", "name": "Egardia", - "documentation": "https://www.home-assistant.io/components/egardia", + "documentation": "https://www.home-assistant.io/integrations/egardia", "requirements": ["pythonegardia==1.0.40"], "dependencies": [], "codeowners": ["@jeroenterheerdt"] diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 2b008c3c370fb7..3353f1fa4d962f 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -1,7 +1,7 @@ { "domain": "eight_sleep", "name": "Eight sleep", - "documentation": "https://www.home-assistant.io/components/eight_sleep", + "documentation": "https://www.home-assistant.io/integrations/eight_sleep", "requirements": [ "pyeight==0.1.1" ], diff --git a/homeassistant/components/eliqonline/manifest.json b/homeassistant/components/eliqonline/manifest.json index 98d94fd009ea3d..0bc242509c3452 100644 --- a/homeassistant/components/eliqonline/manifest.json +++ b/homeassistant/components/eliqonline/manifest.json @@ -1,7 +1,7 @@ { "domain": "eliqonline", "name": "Eliqonline", - "documentation": "https://www.home-assistant.io/components/eliqonline", + "documentation": "https://www.home-assistant.io/integrations/eliqonline", "requirements": [ "eliqonline==1.2.2" ], diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 466f9da7e9012c..75acab5860db1f 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -1,7 +1,7 @@ { "domain": "elkm1", "name": "Elkm1", - "documentation": "https://www.home-assistant.io/components/elkm1", + "documentation": "https://www.home-assistant.io/integrations/elkm1", "requirements": [ "elkm1-lib==0.7.15" ], diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index 04d384416217a8..b4871a805d2d61 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -1,7 +1,7 @@ { "domain": "elv", "name": "ELV PCA", - "documentation": "https://www.home-assistant.io/components/pca", + "documentation": "https://www.home-assistant.io/integrations/pca", "dependencies": [], "codeowners": ["@majuss"], "requirements": ["pypca==0.0.5"] diff --git a/homeassistant/components/emby/manifest.json b/homeassistant/components/emby/manifest.json index 87688733e593ab..12dbb1522061f8 100644 --- a/homeassistant/components/emby/manifest.json +++ b/homeassistant/components/emby/manifest.json @@ -1,7 +1,7 @@ { "domain": "emby", "name": "Emby", - "documentation": "https://www.home-assistant.io/components/emby", + "documentation": "https://www.home-assistant.io/integrations/emby", "requirements": [ "pyemby==1.6" ], diff --git a/homeassistant/components/emoncms/manifest.json b/homeassistant/components/emoncms/manifest.json index 90623c01d1be50..83833d4f79bc5c 100644 --- a/homeassistant/components/emoncms/manifest.json +++ b/homeassistant/components/emoncms/manifest.json @@ -1,7 +1,7 @@ { "domain": "emoncms", "name": "Emoncms", - "documentation": "https://www.home-assistant.io/components/emoncms", + "documentation": "https://www.home-assistant.io/integrations/emoncms", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/emoncms_history/manifest.json b/homeassistant/components/emoncms_history/manifest.json index 0cb09e3fb73b88..80d946f4868dbd 100644 --- a/homeassistant/components/emoncms_history/manifest.json +++ b/homeassistant/components/emoncms_history/manifest.json @@ -1,7 +1,7 @@ { "domain": "emoncms_history", "name": "Emoncms history", - "documentation": "https://www.home-assistant.io/components/emoncms_history", + "documentation": "https://www.home-assistant.io/integrations/emoncms_history", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index 75fcbc4c55500b..9b3b00d20b21e9 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -1,7 +1,7 @@ { "domain": "emulated_hue", "name": "Emulated hue", - "documentation": "https://www.home-assistant.io/components/emulated_hue", + "documentation": "https://www.home-assistant.io/integrations/emulated_hue", "requirements": [ "aiohttp_cors==0.7.0" ], diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index ba68ce94951392..824e5bef7c8da0 100644 --- a/homeassistant/components/emulated_roku/manifest.json +++ b/homeassistant/components/emulated_roku/manifest.json @@ -2,7 +2,7 @@ "domain": "emulated_roku", "name": "Emulated roku", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/emulated_roku", + "documentation": "https://www.home-assistant.io/integrations/emulated_roku", "requirements": [ "emulated_roku==0.1.8" ], diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index d523bd72b720dc..870681fd4a51ed 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -1,7 +1,7 @@ { "domain": "enigma2", "name": "Enigma2", - "documentation": "https://www.home-assistant.io/components/enigma2", + "documentation": "https://www.home-assistant.io/integrations/enigma2", "requirements": [ "openwebifpy==3.1.1" ], diff --git a/homeassistant/components/enocean/manifest.json b/homeassistant/components/enocean/manifest.json index e6f1c5d78262c9..4dffbabd2196a9 100644 --- a/homeassistant/components/enocean/manifest.json +++ b/homeassistant/components/enocean/manifest.json @@ -1,7 +1,7 @@ { "domain": "enocean", "name": "Enocean", - "documentation": "https://www.home-assistant.io/components/enocean", + "documentation": "https://www.home-assistant.io/integrations/enocean", "requirements": [ "enocean==0.50" ], diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 86d2d69cf9b47d..5b5f94f7c8cae2 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -1,7 +1,7 @@ { "domain": "enphase_envoy", "name": "Enphase envoy", - "documentation": "https://www.home-assistant.io/components/enphase_envoy", + "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "requirements": [ "envoy_reader==0.8.6" ], diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index b2b60cff95a486..b0910f165363c2 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "entur_public_transport", "name": "Entur public transport", - "documentation": "https://www.home-assistant.io/components/entur_public_transport", + "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", "requirements": [ "enturclient==0.2.0" ], diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 2ae2006512b03a..c62e1e356b6031 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -1,7 +1,7 @@ { "domain": "environment_canada", "name": "Environment Canada", - "documentation": "https://www.home-assistant.io/components/environment_canada", + "documentation": "https://www.home-assistant.io/integrations/environment_canada", "requirements": [ "env_canada==0.0.25" ], diff --git a/homeassistant/components/envirophat/manifest.json b/homeassistant/components/envirophat/manifest.json index c69a66d43f85eb..ddf69d0d417e41 100644 --- a/homeassistant/components/envirophat/manifest.json +++ b/homeassistant/components/envirophat/manifest.json @@ -1,7 +1,7 @@ { "domain": "envirophat", "name": "Envirophat", - "documentation": "https://www.home-assistant.io/components/envirophat", + "documentation": "https://www.home-assistant.io/integrations/envirophat", "requirements": [ "envirophat==0.0.6", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index b34aa08951ca85..6c5405c75eaae9 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -1,7 +1,7 @@ { "domain": "envisalink", "name": "Envisalink", - "documentation": "https://www.home-assistant.io/components/envisalink", + "documentation": "https://www.home-assistant.io/integrations/envisalink", "requirements": [ "pyenvisalink==3.8" ], diff --git a/homeassistant/components/ephember/manifest.json b/homeassistant/components/ephember/manifest.json index 3fed307aed5f3e..7509e627621396 100644 --- a/homeassistant/components/ephember/manifest.json +++ b/homeassistant/components/ephember/manifest.json @@ -1,7 +1,7 @@ { "domain": "ephember", "name": "Ephember", - "documentation": "https://www.home-assistant.io/components/ephember", + "documentation": "https://www.home-assistant.io/integrations/ephember", "requirements": [ "pyephember==0.2.0" ], diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index e6623b83013ad6..22055e347affe1 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -1,7 +1,7 @@ { "domain": "epson", "name": "Epson", - "documentation": "https://www.home-assistant.io/components/epson", + "documentation": "https://www.home-assistant.io/integrations/epson", "requirements": [ "epson-projector==0.1.3" ], diff --git a/homeassistant/components/epsonworkforce/manifest.json b/homeassistant/components/epsonworkforce/manifest.json index 21f76c3a31f09a..d73b331d5f0d5e 100644 --- a/homeassistant/components/epsonworkforce/manifest.json +++ b/homeassistant/components/epsonworkforce/manifest.json @@ -1,7 +1,7 @@ { "domain": "epsonworkforce", "name": "Epson Workforce", - "documentation": "https://www.home-assistant.io/components/epsonworkforce", + "documentation": "https://www.home-assistant.io/integrations/epsonworkforce", "dependencies": [], "codeowners": ["@ThaStealth"], "requirements": ["epsonprinter==0.0.9"] diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 6d13c79bcec097..1065b94c12ac49 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -1,7 +1,7 @@ { "domain": "eq3btsmart", "name": "Eq3btsmart", - "documentation": "https://www.home-assistant.io/components/eq3btsmart", + "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", "requirements": [ "construct==2.9.45", "python-eq3bt==0.1.9" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 43987cce2c9789..bde64762121ebd 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -2,7 +2,7 @@ "domain": "esphome", "name": "ESPHome", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/esphome", + "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ "aioesphomeapi==2.2.0" ], diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json index aeb3b48311eff4..914c8f1556fa19 100644 --- a/homeassistant/components/essent/manifest.json +++ b/homeassistant/components/essent/manifest.json @@ -1,7 +1,7 @@ { "domain": "essent", "name": "Essent", - "documentation": "https://www.home-assistant.io/components/essent", + "documentation": "https://www.home-assistant.io/integrations/essent", "requirements": ["PyEssent==0.13"], "dependencies": [], "codeowners": ["@TheLastProject"] diff --git a/homeassistant/components/etherscan/manifest.json b/homeassistant/components/etherscan/manifest.json index 452d1c4c475343..f0abf8c7de0da1 100644 --- a/homeassistant/components/etherscan/manifest.json +++ b/homeassistant/components/etherscan/manifest.json @@ -1,7 +1,7 @@ { "domain": "etherscan", "name": "Etherscan", - "documentation": "https://www.home-assistant.io/components/etherscan", + "documentation": "https://www.home-assistant.io/integrations/etherscan", "requirements": [ "python-etherscan-api==0.0.3" ], diff --git a/homeassistant/components/eufy/manifest.json b/homeassistant/components/eufy/manifest.json index ec7f1fe7072231..92a91976500474 100644 --- a/homeassistant/components/eufy/manifest.json +++ b/homeassistant/components/eufy/manifest.json @@ -1,7 +1,7 @@ { "domain": "eufy", "name": "Eufy", - "documentation": "https://www.home-assistant.io/components/eufy", + "documentation": "https://www.home-assistant.io/integrations/eufy", "requirements": [ "lakeside==0.12" ], diff --git a/homeassistant/components/everlights/manifest.json b/homeassistant/components/everlights/manifest.json index 9c2e1b2ae4f667..53e2dbf2cb4eb1 100644 --- a/homeassistant/components/everlights/manifest.json +++ b/homeassistant/components/everlights/manifest.json @@ -1,7 +1,7 @@ { "domain": "everlights", "name": "Everlights", - "documentation": "https://www.home-assistant.io/components/everlights", + "documentation": "https://www.home-assistant.io/integrations/everlights", "requirements": [ "pyeverlights==0.1.0" ], diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 32a57cf20b1c77..5633880be35e58 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -1,7 +1,7 @@ { "domain": "evohome", "name": "Evohome", - "documentation": "https://www.home-assistant.io/components/evohome", + "documentation": "https://www.home-assistant.io/integrations/evohome", "requirements": [ "evohome-async==0.3.3b4" ], diff --git a/homeassistant/components/facebook/manifest.json b/homeassistant/components/facebook/manifest.json index 9632906a25a74e..930047065c58e7 100644 --- a/homeassistant/components/facebook/manifest.json +++ b/homeassistant/components/facebook/manifest.json @@ -1,7 +1,7 @@ { "domain": "facebook", "name": "Facebook", - "documentation": "https://www.home-assistant.io/components/facebook", + "documentation": "https://www.home-assistant.io/integrations/facebook", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/facebox/manifest.json b/homeassistant/components/facebox/manifest.json index 4a3eefc135c563..2c911eb04ef455 100644 --- a/homeassistant/components/facebox/manifest.json +++ b/homeassistant/components/facebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "facebox", "name": "Facebox", - "documentation": "https://www.home-assistant.io/components/facebox", + "documentation": "https://www.home-assistant.io/integrations/facebox", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fail2ban/manifest.json b/homeassistant/components/fail2ban/manifest.json index fc60658a3f2c6a..6ff0c7be0e4e24 100644 --- a/homeassistant/components/fail2ban/manifest.json +++ b/homeassistant/components/fail2ban/manifest.json @@ -1,7 +1,7 @@ { "domain": "fail2ban", "name": "Fail2ban", - "documentation": "https://www.home-assistant.io/components/fail2ban", + "documentation": "https://www.home-assistant.io/integrations/fail2ban", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/familyhub/manifest.json b/homeassistant/components/familyhub/manifest.json index 48a73dfb0300ba..e8aa3ab51b3a27 100644 --- a/homeassistant/components/familyhub/manifest.json +++ b/homeassistant/components/familyhub/manifest.json @@ -1,7 +1,7 @@ { "domain": "familyhub", "name": "Familyhub", - "documentation": "https://www.home-assistant.io/components/familyhub", + "documentation": "https://www.home-assistant.io/integrations/familyhub", "requirements": [ "python-family-hub-local==0.0.2" ], diff --git a/homeassistant/components/fan/manifest.json b/homeassistant/components/fan/manifest.json index 85bb982d2d1f46..0df3b7a850e3cc 100644 --- a/homeassistant/components/fan/manifest.json +++ b/homeassistant/components/fan/manifest.json @@ -1,7 +1,7 @@ { "domain": "fan", "name": "Fan", - "documentation": "https://www.home-assistant.io/components/fan", + "documentation": "https://www.home-assistant.io/integrations/fan", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index f4bf021380c986..3655ce22ba70ee 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -1,7 +1,7 @@ { "domain": "fastdotcom", "name": "Fastdotcom", - "documentation": "https://www.home-assistant.io/components/fastdotcom", + "documentation": "https://www.home-assistant.io/integrations/fastdotcom", "requirements": [ "fastdotcom==0.0.3" ], diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index e458d30073e8a7..6ecc9efffb8406 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -1,7 +1,7 @@ { "domain": "feedreader", "name": "Feedreader", - "documentation": "https://www.home-assistant.io/components/feedreader", + "documentation": "https://www.home-assistant.io/integrations/feedreader", "requirements": [ "feedparser-homeassistant==5.2.2.dev1" ], diff --git a/homeassistant/components/ffmpeg/manifest.json b/homeassistant/components/ffmpeg/manifest.json index 4a3695e7dcc52f..438891eca92c2f 100644 --- a/homeassistant/components/ffmpeg/manifest.json +++ b/homeassistant/components/ffmpeg/manifest.json @@ -1,7 +1,7 @@ { "domain": "ffmpeg", "name": "Ffmpeg", - "documentation": "https://www.home-assistant.io/components/ffmpeg", + "documentation": "https://www.home-assistant.io/integrations/ffmpeg", "requirements": [ "ha-ffmpeg==2.0" ], diff --git a/homeassistant/components/ffmpeg_motion/manifest.json b/homeassistant/components/ffmpeg_motion/manifest.json index e9a0e7b10143f5..5b445dd3094623 100644 --- a/homeassistant/components/ffmpeg_motion/manifest.json +++ b/homeassistant/components/ffmpeg_motion/manifest.json @@ -1,7 +1,7 @@ { "domain": "ffmpeg_motion", "name": "Ffmpeg motion", - "documentation": "https://www.home-assistant.io/components/ffmpeg_motion", + "documentation": "https://www.home-assistant.io/integrations/ffmpeg_motion", "requirements": [], "dependencies": ["ffmpeg"], "codeowners": [] diff --git a/homeassistant/components/ffmpeg_noise/manifest.json b/homeassistant/components/ffmpeg_noise/manifest.json index 71600b311177f5..1bb8e7353dc3b5 100644 --- a/homeassistant/components/ffmpeg_noise/manifest.json +++ b/homeassistant/components/ffmpeg_noise/manifest.json @@ -1,7 +1,7 @@ { "domain": "ffmpeg_noise", "name": "Ffmpeg noise", - "documentation": "https://www.home-assistant.io/components/ffmpeg_noise", + "documentation": "https://www.home-assistant.io/integrations/ffmpeg_noise", "requirements": [], "dependencies": ["ffmpeg"], "codeowners": [] diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 3574e6254ded39..0a5d1316561b1a 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -1,7 +1,7 @@ { "domain": "fibaro", "name": "Fibaro", - "documentation": "https://www.home-assistant.io/components/fibaro", + "documentation": "https://www.home-assistant.io/integrations/fibaro", "requirements": [ "fiblary3==0.1.7" ], diff --git a/homeassistant/components/fido/manifest.json b/homeassistant/components/fido/manifest.json index 343a21ff072fae..638505d5dd9e2d 100644 --- a/homeassistant/components/fido/manifest.json +++ b/homeassistant/components/fido/manifest.json @@ -1,7 +1,7 @@ { "domain": "fido", "name": "Fido", - "documentation": "https://www.home-assistant.io/components/fido", + "documentation": "https://www.home-assistant.io/integrations/fido", "requirements": [ "pyfido==2.1.1" ], diff --git a/homeassistant/components/file/manifest.json b/homeassistant/components/file/manifest.json index 581b0e14156666..07a9cde900fada 100644 --- a/homeassistant/components/file/manifest.json +++ b/homeassistant/components/file/manifest.json @@ -1,7 +1,7 @@ { "domain": "file", "name": "File", - "documentation": "https://www.home-assistant.io/components/file", + "documentation": "https://www.home-assistant.io/integrations/file", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/filesize/manifest.json b/homeassistant/components/filesize/manifest.json index f76bcd27466c64..14e0a6a487c129 100644 --- a/homeassistant/components/filesize/manifest.json +++ b/homeassistant/components/filesize/manifest.json @@ -1,7 +1,7 @@ { "domain": "filesize", "name": "Filesize", - "documentation": "https://www.home-assistant.io/components/filesize", + "documentation": "https://www.home-assistant.io/integrations/filesize", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/filter/manifest.json b/homeassistant/components/filter/manifest.json index 28f061d26f7c5c..f28007ba552882 100644 --- a/homeassistant/components/filter/manifest.json +++ b/homeassistant/components/filter/manifest.json @@ -1,7 +1,7 @@ { "domain": "filter", "name": "Filter", - "documentation": "https://www.home-assistant.io/components/filter", + "documentation": "https://www.home-assistant.io/integrations/filter", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/fints/manifest.json b/homeassistant/components/fints/manifest.json index e3580676290b9a..1d0879c291aac6 100644 --- a/homeassistant/components/fints/manifest.json +++ b/homeassistant/components/fints/manifest.json @@ -1,7 +1,7 @@ { "domain": "fints", "name": "Fints", - "documentation": "https://www.home-assistant.io/components/fints", + "documentation": "https://www.home-assistant.io/integrations/fints", "requirements": [ "fints==1.0.1" ], diff --git a/homeassistant/components/fitbit/manifest.json b/homeassistant/components/fitbit/manifest.json index 6a6316d80a3f6a..f550cb75c5dbe3 100644 --- a/homeassistant/components/fitbit/manifest.json +++ b/homeassistant/components/fitbit/manifest.json @@ -1,7 +1,7 @@ { "domain": "fitbit", "name": "Fitbit", - "documentation": "https://www.home-assistant.io/components/fitbit", + "documentation": "https://www.home-assistant.io/integrations/fitbit", "requirements": [ "fitbit==0.3.1" ], diff --git a/homeassistant/components/fixer/manifest.json b/homeassistant/components/fixer/manifest.json index 1e010bb06ed0c7..e6661ca6ce4e9a 100644 --- a/homeassistant/components/fixer/manifest.json +++ b/homeassistant/components/fixer/manifest.json @@ -1,7 +1,7 @@ { "domain": "fixer", "name": "Fixer", - "documentation": "https://www.home-assistant.io/components/fixer", + "documentation": "https://www.home-assistant.io/integrations/fixer", "requirements": [ "fixerio==1.0.0a0" ], diff --git a/homeassistant/components/fleetgo/manifest.json b/homeassistant/components/fleetgo/manifest.json index c37ece4ebd8860..eece29e167c6e0 100644 --- a/homeassistant/components/fleetgo/manifest.json +++ b/homeassistant/components/fleetgo/manifest.json @@ -1,7 +1,7 @@ { "domain": "fleetgo", "name": "FleetGO", - "documentation": "https://www.home-assistant.io/components/fleetgo", + "documentation": "https://www.home-assistant.io/integrations/fleetgo", "requirements": [ "ritassist==0.9.2" ], diff --git a/homeassistant/components/flexit/manifest.json b/homeassistant/components/flexit/manifest.json index 0ee0e81143cd6c..311904166d5a16 100644 --- a/homeassistant/components/flexit/manifest.json +++ b/homeassistant/components/flexit/manifest.json @@ -1,7 +1,7 @@ { "domain": "flexit", "name": "Flexit", - "documentation": "https://www.home-assistant.io/components/flexit", + "documentation": "https://www.home-assistant.io/integrations/flexit", "requirements": [ "pyflexit==0.3" ], diff --git a/homeassistant/components/flic/manifest.json b/homeassistant/components/flic/manifest.json index 827bcb167c397d..d0651c7fbe9933 100644 --- a/homeassistant/components/flic/manifest.json +++ b/homeassistant/components/flic/manifest.json @@ -1,7 +1,7 @@ { "domain": "flic", "name": "Flic", - "documentation": "https://www.home-assistant.io/components/flic", + "documentation": "https://www.home-assistant.io/integrations/flic", "requirements": [ "pyflic-homeassistant==0.4.dev0" ], diff --git a/homeassistant/components/flock/manifest.json b/homeassistant/components/flock/manifest.json index a5af541eeeef38..ba6f4b1f43f584 100644 --- a/homeassistant/components/flock/manifest.json +++ b/homeassistant/components/flock/manifest.json @@ -1,7 +1,7 @@ { "domain": "flock", "name": "Flock", - "documentation": "https://www.home-assistant.io/components/flock", + "documentation": "https://www.home-assistant.io/integrations/flock", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index 76053f75081735..a5dfaf4027f2ee 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -1,7 +1,7 @@ { "domain": "flunearyou", "name": "Flunearyou", - "documentation": "https://www.home-assistant.io/components/flunearyou", + "documentation": "https://www.home-assistant.io/integrations/flunearyou", "requirements": [ "pyflunearyou==1.0.3" ], diff --git a/homeassistant/components/flux/manifest.json b/homeassistant/components/flux/manifest.json index 9bf3ba09ce7135..e7b0387698ddd6 100644 --- a/homeassistant/components/flux/manifest.json +++ b/homeassistant/components/flux/manifest.json @@ -1,7 +1,7 @@ { "domain": "flux", "name": "Flux", - "documentation": "https://www.home-assistant.io/components/flux", + "documentation": "https://www.home-assistant.io/integrations/flux", "requirements": [], "dependencies": [], "after_dependencies": ["light"], diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 0d00275200caba..4e5531ab4e34f3 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -1,7 +1,7 @@ { "domain": "flux_led", "name": "Flux led", - "documentation": "https://www.home-assistant.io/components/flux_led", + "documentation": "https://www.home-assistant.io/integrations/flux_led", "requirements": [ "flux_led==0.22" ], diff --git a/homeassistant/components/folder/manifest.json b/homeassistant/components/folder/manifest.json index 7a0bf76e0aa310..d4026e7689daf1 100644 --- a/homeassistant/components/folder/manifest.json +++ b/homeassistant/components/folder/manifest.json @@ -1,7 +1,7 @@ { "domain": "folder", "name": "Folder", - "documentation": "https://www.home-assistant.io/components/folder", + "documentation": "https://www.home-assistant.io/integrations/folder", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 1a5b547e5ff21c..17c54a39763952 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -1,7 +1,7 @@ { "domain": "folder_watcher", "name": "Folder watcher", - "documentation": "https://www.home-assistant.io/components/folder_watcher", + "documentation": "https://www.home-assistant.io/integrations/folder_watcher", "requirements": [ "watchdog==0.8.3" ], diff --git a/homeassistant/components/foobot/manifest.json b/homeassistant/components/foobot/manifest.json index 9ed95597e41700..b02149d2bcd441 100644 --- a/homeassistant/components/foobot/manifest.json +++ b/homeassistant/components/foobot/manifest.json @@ -1,7 +1,7 @@ { "domain": "foobot", "name": "Foobot", - "documentation": "https://www.home-assistant.io/components/foobot", + "documentation": "https://www.home-assistant.io/integrations/foobot", "requirements": [ "foobot_async==0.3.1" ], diff --git a/homeassistant/components/fortigate/manifest.json b/homeassistant/components/fortigate/manifest.json index 544ea860778897..ff063d6b7e2d3f 100644 --- a/homeassistant/components/fortigate/manifest.json +++ b/homeassistant/components/fortigate/manifest.json @@ -1,7 +1,7 @@ { "domain": "fortigate", "name": "Fortigate", - "documentation": "https://www.home-assistant.io/components/fortigate", + "documentation": "https://www.home-assistant.io/integrations/fortigate", "dependencies": [], "codeowners": [ "@kifeo" diff --git a/homeassistant/components/fortios/manifest.json b/homeassistant/components/fortios/manifest.json index a63d4b292ada83..4ec5a4fcb2aa6a 100644 --- a/homeassistant/components/fortios/manifest.json +++ b/homeassistant/components/fortios/manifest.json @@ -1,7 +1,7 @@ { "domain": "fortios", "name": "Home Assistant Device Tracker to support FortiOS", - "documentation": "https://www.home-assistant.io/components/fortios/", + "documentation": "https://www.home-assistant.io/integrations/fortios/", "requirements": [ "fortiosapi==0.10.8" ], diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index b05aa956b42a84..b2c44c113ee9ee 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -1,7 +1,7 @@ { "domain": "foscam", "name": "Foscam", - "documentation": "https://www.home-assistant.io/components/foscam", + "documentation": "https://www.home-assistant.io/integrations/foscam", "requirements": [ "libpyfoscam==1.0" ], diff --git a/homeassistant/components/foursquare/manifest.json b/homeassistant/components/foursquare/manifest.json index 84a98ca033625b..c488bf790d507c 100644 --- a/homeassistant/components/foursquare/manifest.json +++ b/homeassistant/components/foursquare/manifest.json @@ -1,7 +1,7 @@ { "domain": "foursquare", "name": "Foursquare", - "documentation": "https://www.home-assistant.io/components/foursquare", + "documentation": "https://www.home-assistant.io/integrations/foursquare", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/free_mobile/manifest.json b/homeassistant/components/free_mobile/manifest.json index b8a40c3fc1d2ae..19d1ce0aff7766 100644 --- a/homeassistant/components/free_mobile/manifest.json +++ b/homeassistant/components/free_mobile/manifest.json @@ -1,7 +1,7 @@ { "domain": "free_mobile", "name": "Free mobile", - "documentation": "https://www.home-assistant.io/components/free_mobile", + "documentation": "https://www.home-assistant.io/integrations/free_mobile", "requirements": [ "freesms==0.1.2" ], diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index 9ee134d41709f3..ac507f59c7f8fc 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "freebox", "name": "Freebox", - "documentation": "https://www.home-assistant.io/components/freebox", + "documentation": "https://www.home-assistant.io/integrations/freebox", "requirements": [ "aiofreepybox==0.0.8" ], diff --git a/homeassistant/components/freedns/manifest.json b/homeassistant/components/freedns/manifest.json index 63f929754db60f..02332c9fb10f87 100644 --- a/homeassistant/components/freedns/manifest.json +++ b/homeassistant/components/freedns/manifest.json @@ -1,7 +1,7 @@ { "domain": "freedns", "name": "Freedns", - "documentation": "https://www.home-assistant.io/components/freedns", + "documentation": "https://www.home-assistant.io/integrations/freedns", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index b2aacbd48ad795..e6c1fee2c958d6 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritz", "name": "Fritz", - "documentation": "https://www.home-assistant.io/components/fritz", + "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ "fritzconnection==0.6.5" ], diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 1ed18140bd2842..a1e28966568881 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritzbox", "name": "Fritzbox", - "documentation": "https://www.home-assistant.io/components/fritzbox", + "documentation": "https://www.home-assistant.io/integrations/fritzbox", "requirements": [ "pyfritzhome==0.4.0" ], diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 19f232ed6677c5..35c27b7ca84f1c 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritzbox_callmonitor", "name": "Fritzbox callmonitor", - "documentation": "https://www.home-assistant.io/components/fritzbox_callmonitor", + "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", "requirements": [ "fritzconnection==0.6.5" ], diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index ac1ce2893e488d..88a7ab5a338fc4 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritzbox_netmonitor", "name": "Fritzbox netmonitor", - "documentation": "https://www.home-assistant.io/components/fritzbox_netmonitor", + "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", "requirements": [ "fritzconnection==0.6.5" ], diff --git a/homeassistant/components/fritzdect/manifest.json b/homeassistant/components/fritzdect/manifest.json index 98d628fe078aaa..e7b59b0713831b 100644 --- a/homeassistant/components/fritzdect/manifest.json +++ b/homeassistant/components/fritzdect/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritzdect", "name": "Fritzdect", - "documentation": "https://www.home-assistant.io/components/fritzdect", + "documentation": "https://www.home-assistant.io/integrations/fritzdect", "requirements": [ "fritzhome==1.0.4" ], diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 8f737e2e1ff7f8..c7e919c95e5636 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -1,7 +1,7 @@ { "domain": "fronius", "name": "Fronius", - "documentation": "https://www.home-assistant.io/components/fronius", + "documentation": "https://www.home-assistant.io/integrations/fronius", "requirements": ["pyfronius==0.4.6"], "dependencies": [], "codeowners": ["@nielstron"] diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index e50989a15df3d4..f1d91879f155eb 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -1,7 +1,7 @@ { "domain": "frontend", "name": "Home Assistant Frontend", - "documentation": "https://www.home-assistant.io/components/frontend", + "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": ["home-assistant-frontend==20190919.1"], "dependencies": [ "api", diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 0e20a509d1f226..17e9f973fd6038 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -1,7 +1,7 @@ { "domain": "frontier_silicon", "name": "Frontier silicon", - "documentation": "https://www.home-assistant.io/components/frontier_silicon", + "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", "requirements": [ "afsapi==0.0.4" ], diff --git a/homeassistant/components/futurenow/manifest.json b/homeassistant/components/futurenow/manifest.json index 5191ab611acf2e..c1b0cd2c0ffce4 100644 --- a/homeassistant/components/futurenow/manifest.json +++ b/homeassistant/components/futurenow/manifest.json @@ -1,7 +1,7 @@ { "domain": "futurenow", "name": "Futurenow", - "documentation": "https://www.home-assistant.io/components/futurenow", + "documentation": "https://www.home-assistant.io/integrations/futurenow", "requirements": [ "pyfnip==0.2" ], diff --git a/homeassistant/components/garadget/manifest.json b/homeassistant/components/garadget/manifest.json index d3781f81d046aa..b86f4e26b11fd9 100644 --- a/homeassistant/components/garadget/manifest.json +++ b/homeassistant/components/garadget/manifest.json @@ -1,7 +1,7 @@ { "domain": "garadget", "name": "Garadget", - "documentation": "https://www.home-assistant.io/components/garadget", + "documentation": "https://www.home-assistant.io/integrations/garadget", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/gc100/manifest.json b/homeassistant/components/gc100/manifest.json index 96d792196ce956..5ea7cb2fb41ed4 100644 --- a/homeassistant/components/gc100/manifest.json +++ b/homeassistant/components/gc100/manifest.json @@ -1,7 +1,7 @@ { "domain": "gc100", "name": "Gc100", - "documentation": "https://www.home-assistant.io/components/gc100", + "documentation": "https://www.home-assistant.io/integrations/gc100", "requirements": [ "python-gc100==1.0.3a" ], diff --git a/homeassistant/components/gearbest/manifest.json b/homeassistant/components/gearbest/manifest.json index 39ceca41d08028..c8bb89c71a9f62 100644 --- a/homeassistant/components/gearbest/manifest.json +++ b/homeassistant/components/gearbest/manifest.json @@ -1,7 +1,7 @@ { "domain": "gearbest", "name": "Gearbest", - "documentation": "https://www.home-assistant.io/components/gearbest", + "documentation": "https://www.home-assistant.io/integrations/gearbest", "requirements": [ "gearbest_parser==1.0.7" ], diff --git a/homeassistant/components/geizhals/manifest.json b/homeassistant/components/geizhals/manifest.json index d53bceaa1455c8..0f81ecbf1be2f3 100644 --- a/homeassistant/components/geizhals/manifest.json +++ b/homeassistant/components/geizhals/manifest.json @@ -1,7 +1,7 @@ { "domain": "geizhals", "name": "Geizhals", - "documentation": "https://www.home-assistant.io/components/geizhals", + "documentation": "https://www.home-assistant.io/integrations/geizhals", "requirements": [ "geizhals==0.0.9" ], diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index e4d3622a562539..9d59d5b991933a 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -1,7 +1,7 @@ { "domain": "generic", "name": "Generic", - "documentation": "https://www.home-assistant.io/components/generic", + "documentation": "https://www.home-assistant.io/integrations/generic", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/generic_thermostat/manifest.json b/homeassistant/components/generic_thermostat/manifest.json index 41fb04c84566b0..283ea3c45fa6b4 100644 --- a/homeassistant/components/generic_thermostat/manifest.json +++ b/homeassistant/components/generic_thermostat/manifest.json @@ -1,7 +1,7 @@ { "domain": "generic_thermostat", "name": "Generic thermostat", - "documentation": "https://www.home-assistant.io/components/generic_thermostat", + "documentation": "https://www.home-assistant.io/integrations/generic_thermostat", "requirements": [], "dependencies": [ "sensor", diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index f2110ffb2f0220..feedf3be607b71 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -1,7 +1,7 @@ { "domain": "geniushub", "name": "Genius Hub", - "documentation": "https://www.home-assistant.io/components/geniushub", + "documentation": "https://www.home-assistant.io/integrations/geniushub", "requirements": [ "geniushub-client==0.6.13" ], diff --git a/homeassistant/components/geo_json_events/manifest.json b/homeassistant/components/geo_json_events/manifest.json index 6ee78fec5620bd..c4c892e88f40af 100644 --- a/homeassistant/components/geo_json_events/manifest.json +++ b/homeassistant/components/geo_json_events/manifest.json @@ -1,7 +1,7 @@ { "domain": "geo_json_events", "name": "Geo json events", - "documentation": "https://www.home-assistant.io/components/geo_json_events", + "documentation": "https://www.home-assistant.io/integrations/geo_json_events", "requirements": [ "geojson_client==0.4" ], diff --git a/homeassistant/components/geo_location/manifest.json b/homeassistant/components/geo_location/manifest.json index 83b4241284e89a..74dd7cbbf87ca0 100644 --- a/homeassistant/components/geo_location/manifest.json +++ b/homeassistant/components/geo_location/manifest.json @@ -1,7 +1,7 @@ { "domain": "geo_location", "name": "Geo location", - "documentation": "https://www.home-assistant.io/components/geo_location", + "documentation": "https://www.home-assistant.io/integrations/geo_location", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index bce6758b0fe9ea..8fd19f6b034411 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -1,7 +1,7 @@ { "domain": "geo_rss_events", "name": "Geo rss events", - "documentation": "https://www.home-assistant.io/components/geo_rss_events", + "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": [ "georss_generic_client==0.2" ], diff --git a/homeassistant/components/geofency/manifest.json b/homeassistant/components/geofency/manifest.json index d593aec46a46de..f7939e2b02591e 100644 --- a/homeassistant/components/geofency/manifest.json +++ b/homeassistant/components/geofency/manifest.json @@ -2,7 +2,7 @@ "domain": "geofency", "name": "Geofency", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/geofency", + "documentation": "https://www.home-assistant.io/integrations/geofency", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index 77f3c64752e9f5..f7aa53b0a3ae50 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -2,7 +2,7 @@ "domain": "geonetnz_quakes", "name": "GeoNet NZ Quakes", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/geonetnz_quakes", + "documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes", "requirements": [ "aio_geojson_geonetnz_quakes==0.10" ], diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index a2c2ae04376bd9..0b5e3c0df9f2bc 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -1,7 +1,7 @@ { "domain": "github", "name": "Github", - "documentation": "https://www.home-assistant.io/components/github", + "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ "PyGithub==1.43.5" ], diff --git a/homeassistant/components/gitlab_ci/manifest.json b/homeassistant/components/gitlab_ci/manifest.json index 4ea04de9e02394..e439e8d7eda85f 100644 --- a/homeassistant/components/gitlab_ci/manifest.json +++ b/homeassistant/components/gitlab_ci/manifest.json @@ -1,7 +1,7 @@ { "domain": "gitlab_ci", "name": "Gitlab ci", - "documentation": "https://www.home-assistant.io/components/gitlab_ci", + "documentation": "https://www.home-assistant.io/integrations/gitlab_ci", "requirements": [ "python-gitlab==1.6.0" ], diff --git a/homeassistant/components/gitter/manifest.json b/homeassistant/components/gitter/manifest.json index 6600e46a4ce953..96df8c4e083c6a 100644 --- a/homeassistant/components/gitter/manifest.json +++ b/homeassistant/components/gitter/manifest.json @@ -1,7 +1,7 @@ { "domain": "gitter", "name": "Gitter", - "documentation": "https://www.home-assistant.io/components/gitter", + "documentation": "https://www.home-assistant.io/integrations/gitter", "requirements": [ "gitterpy==0.1.7" ], diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 621bca8c4309ac..775d208c1c48e5 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -1,7 +1,7 @@ { "domain": "glances", "name": "Glances", - "documentation": "https://www.home-assistant.io/components/glances", + "documentation": "https://www.home-assistant.io/integrations/glances", "requirements": [ "glances_api==0.2.0" ], diff --git a/homeassistant/components/gntp/manifest.json b/homeassistant/components/gntp/manifest.json index 7315e3c7c849be..f1c030125ac096 100644 --- a/homeassistant/components/gntp/manifest.json +++ b/homeassistant/components/gntp/manifest.json @@ -1,7 +1,7 @@ { "domain": "gntp", "name": "Gntp", - "documentation": "https://www.home-assistant.io/components/gntp", + "documentation": "https://www.home-assistant.io/integrations/gntp", "requirements": [ "gntp==1.0.3" ], diff --git a/homeassistant/components/goalfeed/manifest.json b/homeassistant/components/goalfeed/manifest.json index 861abe0b462d9c..a4d7cd50686bc3 100644 --- a/homeassistant/components/goalfeed/manifest.json +++ b/homeassistant/components/goalfeed/manifest.json @@ -1,7 +1,7 @@ { "domain": "goalfeed", "name": "Goalfeed", - "documentation": "https://www.home-assistant.io/components/goalfeed", + "documentation": "https://www.home-assistant.io/integrations/goalfeed", "requirements": [ "pysher==1.0.1" ], diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index 3f3f2c25d0c784..d8878c8b3516d3 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -1,7 +1,7 @@ { "domain": "gogogate2", "name": "Gogogate2", - "documentation": "https://www.home-assistant.io/components/gogogate2", + "documentation": "https://www.home-assistant.io/integrations/gogogate2", "requirements": [ "pygogogate2==0.1.1" ], diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 4c7e82ecfef424..d72cc992f6d100 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -1,7 +1,7 @@ { "domain": "google", "name": "Google", - "documentation": "https://www.home-assistant.io/components/google", + "documentation": "https://www.home-assistant.io/integrations/google", "requirements": [ "google-api-python-client==1.6.4", "httplib2==0.10.3", diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index ff916930216548..d2e016cb5d16b7 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_assistant", "name": "Google assistant", - "documentation": "https://www.home-assistant.io/components/google_assistant", + "documentation": "https://www.home-assistant.io/integrations/google_assistant", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index c8ac0d2e81e588..bc9c71c5a61bfe 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_cloud", "name": "Google Cloud Platform", - "documentation": "https://www.home-assistant.io/components/google_cloud", + "documentation": "https://www.home-assistant.io/integrations/google_cloud", "requirements": [ "google-cloud-texttospeech==0.4.0" ], diff --git a/homeassistant/components/google_domains/manifest.json b/homeassistant/components/google_domains/manifest.json index 190e5860ee60f6..64076434aa591f 100644 --- a/homeassistant/components/google_domains/manifest.json +++ b/homeassistant/components/google_domains/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_domains", "name": "Google domains", - "documentation": "https://www.home-assistant.io/components/google_domains", + "documentation": "https://www.home-assistant.io/integrations/google_domains", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/google_maps/manifest.json b/homeassistant/components/google_maps/manifest.json index ec48e5252a8ab2..30571c33865a27 100644 --- a/homeassistant/components/google_maps/manifest.json +++ b/homeassistant/components/google_maps/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_maps", "name": "Google maps", - "documentation": "https://www.home-assistant.io/components/google_maps", + "documentation": "https://www.home-assistant.io/integrations/google_maps", "requirements": [ "locationsharinglib==4.1.0" ], diff --git a/homeassistant/components/google_pubsub/manifest.json b/homeassistant/components/google_pubsub/manifest.json index ff61ad0e05df57..b23a101ca466cf 100644 --- a/homeassistant/components/google_pubsub/manifest.json +++ b/homeassistant/components/google_pubsub/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_pubsub", "name": "Google pubsub", - "documentation": "https://www.home-assistant.io/components/google_pubsub", + "documentation": "https://www.home-assistant.io/integrations/google_pubsub", "requirements": [ "google-cloud-pubsub==0.39.1" ], diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index cb3cd350c04a92..8b9621b4236fef 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_translate", "name": "Google Translate", - "documentation": "https://www.home-assistant.io/components/google_translate", + "documentation": "https://www.home-assistant.io/integrations/google_translate", "requirements": [ "gTTS-token==1.1.3" ], diff --git a/homeassistant/components/google_travel_time/manifest.json b/homeassistant/components/google_travel_time/manifest.json index eaa168332a63d2..f4113f85a311ce 100644 --- a/homeassistant/components/google_travel_time/manifest.json +++ b/homeassistant/components/google_travel_time/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_travel_time", "name": "Google travel time", - "documentation": "https://www.home-assistant.io/components/google_travel_time", + "documentation": "https://www.home-assistant.io/integrations/google_travel_time", "requirements": [ "googlemaps==2.5.1" ], diff --git a/homeassistant/components/google_wifi/manifest.json b/homeassistant/components/google_wifi/manifest.json index 6e840458207e12..77062cbdd26063 100644 --- a/homeassistant/components/google_wifi/manifest.json +++ b/homeassistant/components/google_wifi/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_wifi", "name": "Google wifi", - "documentation": "https://www.home-assistant.io/components/google_wifi", + "documentation": "https://www.home-assistant.io/integrations/google_wifi", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/gpmdp/manifest.json b/homeassistant/components/gpmdp/manifest.json index 98ab8035023d04..a3c2389478ed9d 100644 --- a/homeassistant/components/gpmdp/manifest.json +++ b/homeassistant/components/gpmdp/manifest.json @@ -1,7 +1,7 @@ { "domain": "gpmdp", "name": "Gpmdp", - "documentation": "https://www.home-assistant.io/components/gpmdp", + "documentation": "https://www.home-assistant.io/integrations/gpmdp", "requirements": [ "websocket-client==0.54.0" ], diff --git a/homeassistant/components/gpsd/manifest.json b/homeassistant/components/gpsd/manifest.json index b35d5cb1850c62..7bb828cacf94b2 100644 --- a/homeassistant/components/gpsd/manifest.json +++ b/homeassistant/components/gpsd/manifest.json @@ -1,7 +1,7 @@ { "domain": "gpsd", "name": "Gpsd", - "documentation": "https://www.home-assistant.io/components/gpsd", + "documentation": "https://www.home-assistant.io/integrations/gpsd", "requirements": [ "gps3==0.33.3" ], diff --git a/homeassistant/components/gpslogger/manifest.json b/homeassistant/components/gpslogger/manifest.json index f039e50914b107..cbfd79671eb1c1 100644 --- a/homeassistant/components/gpslogger/manifest.json +++ b/homeassistant/components/gpslogger/manifest.json @@ -2,7 +2,7 @@ "domain": "gpslogger", "name": "Gpslogger", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/gpslogger", + "documentation": "https://www.home-assistant.io/integrations/gpslogger", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/graphite/manifest.json b/homeassistant/components/graphite/manifest.json index a5eefc5af04375..49748128258e42 100644 --- a/homeassistant/components/graphite/manifest.json +++ b/homeassistant/components/graphite/manifest.json @@ -1,7 +1,7 @@ { "domain": "graphite", "name": "Graphite", - "documentation": "https://www.home-assistant.io/components/graphite", + "documentation": "https://www.home-assistant.io/integrations/graphite", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json index 7bfb87ede474ed..1e9569e850921f 100644 --- a/homeassistant/components/greeneye_monitor/manifest.json +++ b/homeassistant/components/greeneye_monitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "greeneye_monitor", "name": "Greeneye monitor", - "documentation": "https://www.home-assistant.io/components/greeneye_monitor", + "documentation": "https://www.home-assistant.io/integrations/greeneye_monitor", "requirements": [ "greeneye_monitor==1.0" ], diff --git a/homeassistant/components/greenwave/manifest.json b/homeassistant/components/greenwave/manifest.json index 1032b5eaf2a2ed..20a49e834b0a65 100644 --- a/homeassistant/components/greenwave/manifest.json +++ b/homeassistant/components/greenwave/manifest.json @@ -1,7 +1,7 @@ { "domain": "greenwave", "name": "Greenwave", - "documentation": "https://www.home-assistant.io/components/greenwave", + "documentation": "https://www.home-assistant.io/integrations/greenwave", "requirements": [ "greenwavereality==0.5.1" ], diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index aa99e20a4dfe46..195227ca242cde 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -1,7 +1,7 @@ { "domain": "group", "name": "Group", - "documentation": "https://www.home-assistant.io/components/group", + "documentation": "https://www.home-assistant.io/integrations/group", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index a6a1d2b8aebb7a..0d4508c26dcece 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -1,7 +1,7 @@ { "domain": "growatt_server", "name": "Growatt Server", - "documentation": "https://www.home-assistant.io/components/growatt_server/", + "documentation": "https://www.home-assistant.io/integrations/growatt_server/", "requirements": [ "growattServer==0.0.1" ], diff --git a/homeassistant/components/gstreamer/manifest.json b/homeassistant/components/gstreamer/manifest.json index 6bfb8abbe0b5b8..66cae733d9c9f8 100644 --- a/homeassistant/components/gstreamer/manifest.json +++ b/homeassistant/components/gstreamer/manifest.json @@ -1,7 +1,7 @@ { "domain": "gstreamer", "name": "Gstreamer", - "documentation": "https://www.home-assistant.io/components/gstreamer", + "documentation": "https://www.home-assistant.io/integrations/gstreamer", "requirements": [ "gstreamer-player==1.1.2" ], diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json index 1c7ddbd65ee90d..b25134bb79e237 100644 --- a/homeassistant/components/gtfs/manifest.json +++ b/homeassistant/components/gtfs/manifest.json @@ -1,7 +1,7 @@ { "domain": "gtfs", "name": "Gtfs", - "documentation": "https://www.home-assistant.io/components/gtfs", + "documentation": "https://www.home-assistant.io/integrations/gtfs", "requirements": [ "pygtfs==0.1.5" ], diff --git a/homeassistant/components/gtt/manifest.json b/homeassistant/components/gtt/manifest.json index 142261fe155717..217b17555549ba 100644 --- a/homeassistant/components/gtt/manifest.json +++ b/homeassistant/components/gtt/manifest.json @@ -1,7 +1,7 @@ { "domain": "gtt", "name": "Gtt", - "documentation": "https://www.home-assistant.io/components/gtt", + "documentation": "https://www.home-assistant.io/integrations/gtt", "requirements": [ "pygtt==1.1.2" ], diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index b8e622823d31d3..a3ac10a1c6d8a8 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -1,7 +1,7 @@ { "domain": "habitica", "name": "Habitica", - "documentation": "https://www.home-assistant.io/components/habitica", + "documentation": "https://www.home-assistant.io/integrations/habitica", "requirements": [ "habitipy==0.2.0" ], diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index 4a90e9c977ec9a..2f222b3c16e919 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -2,7 +2,7 @@ "domain": "hangouts", "name": "Hangouts", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/hangouts", + "documentation": "https://www.home-assistant.io/integrations/hangouts", "requirements": [ "hangups==0.4.9" ], diff --git a/homeassistant/components/harman_kardon_avr/manifest.json b/homeassistant/components/harman_kardon_avr/manifest.json index eecbf0edd63e77..6bd64942619725 100644 --- a/homeassistant/components/harman_kardon_avr/manifest.json +++ b/homeassistant/components/harman_kardon_avr/manifest.json @@ -1,7 +1,7 @@ { "domain": "harman_kardon_avr", "name": "Harman kardon avr", - "documentation": "https://www.home-assistant.io/components/harman_kardon_avr", + "documentation": "https://www.home-assistant.io/integrations/harman_kardon_avr", "requirements": [ "hkavr==0.0.5" ], diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index a957db0675fb94..af4427c5db3208 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -1,7 +1,7 @@ { "domain": "harmony", "name": "Harmony", - "documentation": "https://www.home-assistant.io/components/harmony", + "documentation": "https://www.home-assistant.io/integrations/harmony", "requirements": [ "aioharmony==0.1.13" ], diff --git a/homeassistant/components/haveibeenpwned/manifest.json b/homeassistant/components/haveibeenpwned/manifest.json index 40572f82ea80eb..00c7b7a19b8b70 100644 --- a/homeassistant/components/haveibeenpwned/manifest.json +++ b/homeassistant/components/haveibeenpwned/manifest.json @@ -1,7 +1,7 @@ { "domain": "haveibeenpwned", "name": "Haveibeenpwned", - "documentation": "https://www.home-assistant.io/components/haveibeenpwned", + "documentation": "https://www.home-assistant.io/integrations/haveibeenpwned", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/hddtemp/manifest.json b/homeassistant/components/hddtemp/manifest.json index 2d34d3b4e7b645..484886aff2112b 100644 --- a/homeassistant/components/hddtemp/manifest.json +++ b/homeassistant/components/hddtemp/manifest.json @@ -1,7 +1,7 @@ { "domain": "hddtemp", "name": "Hddtemp", - "documentation": "https://www.home-assistant.io/components/hddtemp", + "documentation": "https://www.home-assistant.io/integrations/hddtemp", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index b59d5622821db3..7c877f5c2a7b45 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -1,7 +1,7 @@ { "domain": "hdmi_cec", "name": "Hdmi cec", - "documentation": "https://www.home-assistant.io/components/hdmi_cec", + "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", "requirements": [ "pyCEC==0.4.13" ], diff --git a/homeassistant/components/heatmiser/manifest.json b/homeassistant/components/heatmiser/manifest.json index 0a11aecd079d9b..b3882c63c51f28 100644 --- a/homeassistant/components/heatmiser/manifest.json +++ b/homeassistant/components/heatmiser/manifest.json @@ -1,7 +1,7 @@ { "domain": "heatmiser", "name": "Heatmiser", - "documentation": "https://www.home-assistant.io/components/heatmiser", + "documentation": "https://www.home-assistant.io/integrations/heatmiser", "requirements": [ "heatmiserV3==0.9.1" ], diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index eb9ef258a3cbd2..fb21a43356f657 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -2,7 +2,7 @@ "domain": "heos", "name": "HEOS", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/heos", + "documentation": "https://www.home-assistant.io/integrations/heos", "requirements": [ "pyheos==0.6.0" ], diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index e26e2e1d6ea57c..0f2bde253de5a9 100755 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -1,7 +1,7 @@ { "domain": "here_travel_time", "name": "HERE travel time", - "documentation": "https://www.home-assistant.io/components/here_travel_time", + "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "requirements": [ "herepy==0.6.3.1" ], diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index bee53c89cdf729..78917a5351bcd9 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -1,7 +1,7 @@ { "domain": "hikvision", "name": "Hikvision", - "documentation": "https://www.home-assistant.io/components/hikvision", + "documentation": "https://www.home-assistant.io/integrations/hikvision", "requirements": [ "pyhik==0.2.3" ], diff --git a/homeassistant/components/hikvisioncam/manifest.json b/homeassistant/components/hikvisioncam/manifest.json index f2bb0822d17c19..8dcef17fad6cde 100644 --- a/homeassistant/components/hikvisioncam/manifest.json +++ b/homeassistant/components/hikvisioncam/manifest.json @@ -1,7 +1,7 @@ { "domain": "hikvisioncam", "name": "Hikvisioncam", - "documentation": "https://www.home-assistant.io/components/hikvisioncam", + "documentation": "https://www.home-assistant.io/integrations/hikvisioncam", "requirements": [ "hikvision==0.4" ], diff --git a/homeassistant/components/hipchat/manifest.json b/homeassistant/components/hipchat/manifest.json index d49e05a5416f9c..9d563719a2e3f5 100644 --- a/homeassistant/components/hipchat/manifest.json +++ b/homeassistant/components/hipchat/manifest.json @@ -1,7 +1,7 @@ { "domain": "hipchat", "name": "Hipchat", - "documentation": "https://www.home-assistant.io/components/hipchat", + "documentation": "https://www.home-assistant.io/integrations/hipchat", "requirements": [ "hipnotify==1.0.8" ], diff --git a/homeassistant/components/history/manifest.json b/homeassistant/components/history/manifest.json index e0989958626a17..00789c905c225d 100644 --- a/homeassistant/components/history/manifest.json +++ b/homeassistant/components/history/manifest.json @@ -1,7 +1,7 @@ { "domain": "history", "name": "History", - "documentation": "https://www.home-assistant.io/components/history", + "documentation": "https://www.home-assistant.io/integrations/history", "requirements": [], "dependencies": [ "http", diff --git a/homeassistant/components/history_graph/manifest.json b/homeassistant/components/history_graph/manifest.json index fa0d437a700c96..a4a0eb4d3e9125 100644 --- a/homeassistant/components/history_graph/manifest.json +++ b/homeassistant/components/history_graph/manifest.json @@ -1,7 +1,7 @@ { "domain": "history_graph", "name": "History graph", - "documentation": "https://www.home-assistant.io/components/history_graph", + "documentation": "https://www.home-assistant.io/integrations/history_graph", "requirements": [], "dependencies": [ "history" diff --git a/homeassistant/components/history_stats/manifest.json b/homeassistant/components/history_stats/manifest.json index ea0abd87c28c42..55a3449f4d682e 100644 --- a/homeassistant/components/history_stats/manifest.json +++ b/homeassistant/components/history_stats/manifest.json @@ -1,7 +1,7 @@ { "domain": "history_stats", "name": "History stats", - "documentation": "https://www.home-assistant.io/components/history_stats", + "documentation": "https://www.home-assistant.io/integrations/history_stats", "requirements": [], "dependencies": [ "history" diff --git a/homeassistant/components/hitron_coda/manifest.json b/homeassistant/components/hitron_coda/manifest.json index 9f3c20fcca534b..6b0492881fb7ff 100644 --- a/homeassistant/components/hitron_coda/manifest.json +++ b/homeassistant/components/hitron_coda/manifest.json @@ -1,7 +1,7 @@ { "domain": "hitron_coda", "name": "Hitron coda", - "documentation": "https://www.home-assistant.io/components/hitron_coda", + "documentation": "https://www.home-assistant.io/integrations/hitron_coda", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index d9fae3fe54b936..4164283f9f88bd 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -1,7 +1,7 @@ { "domain": "hive", "name": "Hive", - "documentation": "https://www.home-assistant.io/components/hive", + "documentation": "https://www.home-assistant.io/integrations/hive", "requirements": [ "pyhiveapi==0.2.19.2" ], diff --git a/homeassistant/components/hlk_sw16/manifest.json b/homeassistant/components/hlk_sw16/manifest.json index 5266b81ab0383d..037df20b35df37 100644 --- a/homeassistant/components/hlk_sw16/manifest.json +++ b/homeassistant/components/hlk_sw16/manifest.json @@ -1,7 +1,7 @@ { "domain": "hlk_sw16", "name": "Hlk sw16", - "documentation": "https://www.home-assistant.io/components/hlk_sw16", + "documentation": "https://www.home-assistant.io/integrations/hlk_sw16", "requirements": [ "hlk-sw16==0.0.7" ], diff --git a/homeassistant/components/homeassistant/manifest.json b/homeassistant/components/homeassistant/manifest.json index b612c3a9fa6454..b4c03047a7a776 100644 --- a/homeassistant/components/homeassistant/manifest.json +++ b/homeassistant/components/homeassistant/manifest.json @@ -1,7 +1,7 @@ { "domain": "homeassistant", "name": "Home Assistant Core Integration", - "documentation": "https://www.home-assistant.io/components/homeassistant", + "documentation": "https://www.home-assistant.io/integrations/homeassistant", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index ebb0895bd7a100..c0ab61d8568642 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -1,7 +1,7 @@ { "domain": "homekit", "name": "Homekit", - "documentation": "https://www.home-assistant.io/components/homekit", + "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ "HAP-python==2.6.0" ], diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 70f6f6a3ce40cf..2a6602813119ab 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -2,7 +2,7 @@ "domain": "homekit_controller", "name": "Homekit controller", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/homekit_controller", + "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": [ "homekit[IP]==0.15.0" ], diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 3c350e75730075..260e54e65c4b60 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -1,7 +1,7 @@ { "domain": "homematic", "name": "Homematic", - "documentation": "https://www.home-assistant.io/components/homematic", + "documentation": "https://www.home-assistant.io/integrations/homematic", "requirements": [ "pyhomematic==0.1.60" ], diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 2075f88ded25ab..40c8c7c359893a 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "homematicip_cloud", "name": "Homematicip cloud", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/homematicip_cloud", + "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "requirements": [ "homematicip==0.10.12" ], diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index cdbbffb8d3686f..f2929fb655ebce 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -1,7 +1,7 @@ { "domain": "homeworks", "name": "Homeworks", - "documentation": "https://www.home-assistant.io/components/homeworks", + "documentation": "https://www.home-assistant.io/integrations/homeworks", "requirements": [ "pyhomeworks==0.0.6" ], diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index b50c7f61dd50e3..9d644de4445597 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -1,7 +1,7 @@ { "domain": "honeywell", "name": "Honeywell", - "documentation": "https://www.home-assistant.io/components/honeywell", + "documentation": "https://www.home-assistant.io/integrations/honeywell", "requirements": [ "somecomfort==0.5.2" ], diff --git a/homeassistant/components/hook/manifest.json b/homeassistant/components/hook/manifest.json index d9898a71f8b717..035354c969a82d 100644 --- a/homeassistant/components/hook/manifest.json +++ b/homeassistant/components/hook/manifest.json @@ -1,7 +1,7 @@ { "domain": "hook", "name": "Hook", - "documentation": "https://www.home-assistant.io/components/hook", + "documentation": "https://www.home-assistant.io/integrations/hook", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/horizon/manifest.json b/homeassistant/components/horizon/manifest.json index 2916e81ce4f4e2..4ba3a61d8b7ec0 100644 --- a/homeassistant/components/horizon/manifest.json +++ b/homeassistant/components/horizon/manifest.json @@ -1,7 +1,7 @@ { "domain": "horizon", "name": "Horizon", - "documentation": "https://www.home-assistant.io/components/horizon", + "documentation": "https://www.home-assistant.io/integrations/horizon", "requirements": [ "horimote==0.4.1" ], diff --git a/homeassistant/components/hp_ilo/manifest.json b/homeassistant/components/hp_ilo/manifest.json index a3d5853541f58b..3dc591cac4cafc 100644 --- a/homeassistant/components/hp_ilo/manifest.json +++ b/homeassistant/components/hp_ilo/manifest.json @@ -1,7 +1,7 @@ { "domain": "hp_ilo", "name": "Hp ilo", - "documentation": "https://www.home-assistant.io/components/hp_ilo", + "documentation": "https://www.home-assistant.io/integrations/hp_ilo", "requirements": [ "python-hpilo==4.3" ], diff --git a/homeassistant/components/html5/manifest.json b/homeassistant/components/html5/manifest.json index 7b43ec44ef3868..667a57891821a7 100644 --- a/homeassistant/components/html5/manifest.json +++ b/homeassistant/components/html5/manifest.json @@ -1,7 +1,7 @@ { "domain": "html5", "name": "HTML5 Notifications", - "documentation": "https://www.home-assistant.io/components/html5", + "documentation": "https://www.home-assistant.io/integrations/html5", "requirements": [ "pywebpush==1.9.2" ], diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index 0bc5586445dd60..6db8b041cfd75d 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -1,7 +1,7 @@ { "domain": "http", "name": "HTTP", - "documentation": "https://www.home-assistant.io/components/http", + "documentation": "https://www.home-assistant.io/integrations/http", "requirements": [ "aiohttp_cors==0.7.0" ], diff --git a/homeassistant/components/htu21d/manifest.json b/homeassistant/components/htu21d/manifest.json index 70093df9b55fd2..14b0d7b3f15e9f 100644 --- a/homeassistant/components/htu21d/manifest.json +++ b/homeassistant/components/htu21d/manifest.json @@ -1,7 +1,7 @@ { "domain": "htu21d", "name": "Htu21d", - "documentation": "https://www.home-assistant.io/components/htu21d", + "documentation": "https://www.home-assistant.io/integrations/htu21d", "requirements": [ "i2csense==0.0.4", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 3af23be4f0b5f2..5d559cc60c5c3e 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -1,7 +1,7 @@ { "domain": "huawei_lte", "name": "Huawei LTE", - "documentation": "https://www.home-assistant.io/components/huawei_lte", + "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.1", "huawei-lte-api==1.3.0" diff --git a/homeassistant/components/huawei_router/manifest.json b/homeassistant/components/huawei_router/manifest.json index 54fd155b557bd1..2affcb8ee2e953 100644 --- a/homeassistant/components/huawei_router/manifest.json +++ b/homeassistant/components/huawei_router/manifest.json @@ -1,7 +1,7 @@ { "domain": "huawei_router", "name": "Huawei router", - "documentation": "https://www.home-assistant.io/components/huawei_router", + "documentation": "https://www.home-assistant.io/integrations/huawei_router", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index cb37dd3036f245..9a3e478d108f67 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -2,7 +2,7 @@ "domain": "hue", "name": "Philips Hue", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/hue", + "documentation": "https://www.home-assistant.io/integrations/hue", "requirements": [ "aiohue==1.9.2" ], diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index c4e1bcc28e8534..3aeffd025ee4bf 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -1,7 +1,7 @@ { "domain": "hunterdouglas_powerview", "name": "Hunterdouglas powerview", - "documentation": "https://www.home-assistant.io/components/hunterdouglas_powerview", + "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", "requirements": [ "aiopvapi==1.6.14" ], diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index 6d332a28bcc45d..eaa0d10622a331 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -1,7 +1,7 @@ { "domain": "hydrawise", "name": "Hydrawise", - "documentation": "https://www.home-assistant.io/components/hydrawise", + "documentation": "https://www.home-assistant.io/integrations/hydrawise", "requirements": [ "hydrawiser==0.1.1" ], diff --git a/homeassistant/components/hydroquebec/manifest.json b/homeassistant/components/hydroquebec/manifest.json index efea5ce0f2e0c2..dbe8af0b41b125 100644 --- a/homeassistant/components/hydroquebec/manifest.json +++ b/homeassistant/components/hydroquebec/manifest.json @@ -1,7 +1,7 @@ { "domain": "hydroquebec", "name": "Hydroquebec", - "documentation": "https://www.home-assistant.io/components/hydroquebec", + "documentation": "https://www.home-assistant.io/integrations/hydroquebec", "requirements": [ "pyhydroquebec==2.2.2" ], diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 980c227944a640..e4ac9c0897b912 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -1,7 +1,7 @@ { "domain": "hyperion", "name": "Hyperion", - "documentation": "https://www.home-assistant.io/components/hyperion", + "documentation": "https://www.home-assistant.io/integrations/hyperion", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index df492d136fd016..6e575ef17bcca9 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -1,7 +1,7 @@ { "domain": "ialarm", "name": "Ialarm", - "documentation": "https://www.home-assistant.io/components/ialarm", + "documentation": "https://www.home-assistant.io/integrations/ialarm", "requirements": [ "pyialarm==0.3" ], diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index 25e02536897915..e883aec371c06b 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -2,7 +2,7 @@ "domain": "iaqualink", "name": "Jandy iAqualink", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/iaqualink/", + "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "dependencies": [], "codeowners": [ "@flz" diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 5f2075a0fd6316..d3924ee61a8b21 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "icloud", "name": "Icloud", - "documentation": "https://www.home-assistant.io/components/icloud", + "documentation": "https://www.home-assistant.io/integrations/icloud", "requirements": [ "pyicloud==0.9.1" ], diff --git a/homeassistant/components/idteck_prox/manifest.json b/homeassistant/components/idteck_prox/manifest.json index 8df144a0f8150b..7a8e3955686c1c 100644 --- a/homeassistant/components/idteck_prox/manifest.json +++ b/homeassistant/components/idteck_prox/manifest.json @@ -1,7 +1,7 @@ { "domain": "idteck_prox", "name": "Idteck prox", - "documentation": "https://www.home-assistant.io/components/idteck_prox", + "documentation": "https://www.home-assistant.io/integrations/idteck_prox", "requirements": [ "rfk101py==0.0.1" ], diff --git a/homeassistant/components/ifttt/manifest.json b/homeassistant/components/ifttt/manifest.json index 58490569e6537e..975a3128f36201 100644 --- a/homeassistant/components/ifttt/manifest.json +++ b/homeassistant/components/ifttt/manifest.json @@ -2,7 +2,7 @@ "domain": "ifttt", "name": "Ifttt", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ifttt", + "documentation": "https://www.home-assistant.io/integrations/ifttt", "requirements": [ "pyfttt==0.3" ], diff --git a/homeassistant/components/iglo/manifest.json b/homeassistant/components/iglo/manifest.json index 4d84c27cd93f82..8771ada45e1cbc 100644 --- a/homeassistant/components/iglo/manifest.json +++ b/homeassistant/components/iglo/manifest.json @@ -1,7 +1,7 @@ { "domain": "iglo", "name": "Iglo", - "documentation": "https://www.home-assistant.io/components/iglo", + "documentation": "https://www.home-assistant.io/integrations/iglo", "requirements": [ "iglo==1.2.7" ], diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json index d2ab3ad449cd11..edb77f1dc6d570 100644 --- a/homeassistant/components/ign_sismologia/manifest.json +++ b/homeassistant/components/ign_sismologia/manifest.json @@ -1,7 +1,7 @@ { "domain": "ign_sismologia", "name": "IGN Sismologia", - "documentation": "https://www.home-assistant.io/components/ign_sismologia", + "documentation": "https://www.home-assistant.io/integrations/ign_sismologia", "requirements": [ "georss_ign_sismologia_client==0.2" ], diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index 25d0317078f6fb..a415b0e3103a90 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -1,7 +1,7 @@ { "domain": "ihc", "name": "Ihc", - "documentation": "https://www.home-assistant.io/components/ihc", + "documentation": "https://www.home-assistant.io/integrations/ihc", "requirements": [ "defusedxml==0.6.0", "ihcsdk==2.3.0" diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index f3a7121c0b4ff3..4a96e9828cb453 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -1,7 +1,7 @@ { "domain": "image_processing", "name": "Image processing", - "documentation": "https://www.home-assistant.io/components/image_processing", + "documentation": "https://www.home-assistant.io/integrations/image_processing", "requirements": [ "pillow==6.1.0" ], diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 9e0f387a7a6ec6..20767a0d49df90 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -1,7 +1,7 @@ { "domain": "imap", "name": "Imap", - "documentation": "https://www.home-assistant.io/components/imap", + "documentation": "https://www.home-assistant.io/integrations/imap", "requirements": [ "aioimaplib==0.7.15" ], diff --git a/homeassistant/components/imap_email_content/manifest.json b/homeassistant/components/imap_email_content/manifest.json index a1e2c616832f2e..e689cb859da552 100644 --- a/homeassistant/components/imap_email_content/manifest.json +++ b/homeassistant/components/imap_email_content/manifest.json @@ -1,7 +1,7 @@ { "domain": "imap_email_content", "name": "Imap email content", - "documentation": "https://www.home-assistant.io/components/imap_email_content", + "documentation": "https://www.home-assistant.io/integrations/imap_email_content", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index c26ba27a29ae42..4bdf43f89578d2 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -1,7 +1,7 @@ { "domain": "incomfort", "name": "Intergas InComfort/Intouch Lan2RF gateway", - "documentation": "https://www.home-assistant.io/components/incomfort", + "documentation": "https://www.home-assistant.io/integrations/incomfort", "requirements": [ "incomfort-client==0.3.5" ], diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index feda5da732cab7..df936eafa903e0 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -1,7 +1,7 @@ { "domain": "influxdb", "name": "Influxdb", - "documentation": "https://www.home-assistant.io/components/influxdb", + "documentation": "https://www.home-assistant.io/integrations/influxdb", "requirements": [ "influxdb==5.2.3" ], diff --git a/homeassistant/components/input_boolean/manifest.json b/homeassistant/components/input_boolean/manifest.json index e233b5635fc77e..09ae235e6b878a 100644 --- a/homeassistant/components/input_boolean/manifest.json +++ b/homeassistant/components/input_boolean/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_boolean", "name": "Input boolean", - "documentation": "https://www.home-assistant.io/components/input_boolean", + "documentation": "https://www.home-assistant.io/integrations/input_boolean", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/input_datetime/manifest.json b/homeassistant/components/input_datetime/manifest.json index 287777e2ccf5a5..9808c45aa74b3c 100644 --- a/homeassistant/components/input_datetime/manifest.json +++ b/homeassistant/components/input_datetime/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_datetime", "name": "Input datetime", - "documentation": "https://www.home-assistant.io/components/input_datetime", + "documentation": "https://www.home-assistant.io/integrations/input_datetime", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/input_number/manifest.json b/homeassistant/components/input_number/manifest.json index 2015b8ea734f3d..31e00d0fce8d93 100644 --- a/homeassistant/components/input_number/manifest.json +++ b/homeassistant/components/input_number/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_number", "name": "Input number", - "documentation": "https://www.home-assistant.io/components/input_number", + "documentation": "https://www.home-assistant.io/integrations/input_number", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/input_select/manifest.json b/homeassistant/components/input_select/manifest.json index a71fb53a5d1b45..d71674fd40cce0 100644 --- a/homeassistant/components/input_select/manifest.json +++ b/homeassistant/components/input_select/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_select", "name": "Input select", - "documentation": "https://www.home-assistant.io/components/input_select", + "documentation": "https://www.home-assistant.io/integrations/input_select", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/input_text/manifest.json b/homeassistant/components/input_text/manifest.json index 6362e6793192f3..eaddaf49b84a24 100644 --- a/homeassistant/components/input_text/manifest.json +++ b/homeassistant/components/input_text/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_text", "name": "Input text", - "documentation": "https://www.home-assistant.io/components/input_text", + "documentation": "https://www.home-assistant.io/integrations/input_text", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 5fcc2c5b507ef3..c8821f3b176571 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -1,7 +1,7 @@ { "domain": "insteon", "name": "Insteon", - "documentation": "https://www.home-assistant.io/components/insteon", + "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": [ "insteonplm==0.16.5" ], diff --git a/homeassistant/components/integration/manifest.json b/homeassistant/components/integration/manifest.json index 869ad2766f90bc..d910e7bdc78735 100644 --- a/homeassistant/components/integration/manifest.json +++ b/homeassistant/components/integration/manifest.json @@ -1,7 +1,7 @@ { "domain": "integration", "name": "Integration", - "documentation": "https://www.home-assistant.io/components/integration", + "documentation": "https://www.home-assistant.io/integrations/integration", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/intent_script/manifest.json b/homeassistant/components/intent_script/manifest.json index 891be6b21802ed..f2d9a338560bc9 100644 --- a/homeassistant/components/intent_script/manifest.json +++ b/homeassistant/components/intent_script/manifest.json @@ -1,7 +1,7 @@ { "domain": "intent_script", "name": "Intent script", - "documentation": "https://www.home-assistant.io/components/intent_script", + "documentation": "https://www.home-assistant.io/integrations/intent_script", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ios/manifest.json b/homeassistant/components/ios/manifest.json index 28c9ea1e952b30..6e011a43ded05d 100644 --- a/homeassistant/components/ios/manifest.json +++ b/homeassistant/components/ios/manifest.json @@ -2,7 +2,7 @@ "domain": "ios", "name": "Ios", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ios", + "documentation": "https://www.home-assistant.io/integrations/ios", "requirements": [], "dependencies": [ "device_tracker", diff --git a/homeassistant/components/iota/manifest.json b/homeassistant/components/iota/manifest.json index d83defbbec3332..018ea4ac167ecc 100644 --- a/homeassistant/components/iota/manifest.json +++ b/homeassistant/components/iota/manifest.json @@ -1,7 +1,7 @@ { "domain": "iota", "name": "Iota", - "documentation": "https://www.home-assistant.io/components/iota", + "documentation": "https://www.home-assistant.io/integrations/iota", "requirements": [ "pyota==2.0.5" ], diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json index 0547628b4bfd2c..c3b1e27c77acc9 100644 --- a/homeassistant/components/iperf3/manifest.json +++ b/homeassistant/components/iperf3/manifest.json @@ -1,7 +1,7 @@ { "domain": "iperf3", "name": "Iperf3", - "documentation": "https://www.home-assistant.io/components/iperf3", + "documentation": "https://www.home-assistant.io/integrations/iperf3", "requirements": [ "iperf3==0.1.11" ], diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 093ccbf6a5b519..47759759a56600 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -2,7 +2,7 @@ "domain": "ipma", "name": "Ipma", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ipma", + "documentation": "https://www.home-assistant.io/integrations/ipma", "requirements": [ "pyipma==1.2.1" ], diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 7392c931f48339..caf422938b2599 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -2,7 +2,7 @@ "domain": "iqvia", "name": "IQVIA", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/iqvia", + "documentation": "https://www.home-assistant.io/integrations/iqvia", "requirements": [ "numpy==1.17.1", "pyiqvia==0.2.1" diff --git a/homeassistant/components/irish_rail_transport/manifest.json b/homeassistant/components/irish_rail_transport/manifest.json index 5961400e68ec8c..da15d87ab02556 100644 --- a/homeassistant/components/irish_rail_transport/manifest.json +++ b/homeassistant/components/irish_rail_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "irish_rail_transport", "name": "Irish rail transport", - "documentation": "https://www.home-assistant.io/components/irish_rail_transport", + "documentation": "https://www.home-assistant.io/integrations/irish_rail_transport", "requirements": [ "pyirishrail==0.0.2" ], diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index 4dc9e2cb7c3f53..035b61d0f2d1b9 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -1,7 +1,7 @@ { "domain": "islamic_prayer_times", "name": "Islamic prayer times", - "documentation": "https://www.home-assistant.io/components/islamic_prayer_times", + "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", "requirements": [ "prayer_times_calculator==0.0.3" ], diff --git a/homeassistant/components/iss/manifest.json b/homeassistant/components/iss/manifest.json index dc71e81ac0808d..72521e67af916f 100644 --- a/homeassistant/components/iss/manifest.json +++ b/homeassistant/components/iss/manifest.json @@ -1,7 +1,7 @@ { "domain": "iss", "name": "Iss", - "documentation": "https://www.home-assistant.io/components/iss", + "documentation": "https://www.home-assistant.io/integrations/iss", "requirements": [ "pyiss==1.0.1" ], diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 0dd0f1eae80a0b..759e1b78e8ed70 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -1,7 +1,7 @@ { "domain": "isy994", "name": "Isy994", - "documentation": "https://www.home-assistant.io/components/isy994", + "documentation": "https://www.home-assistant.io/integrations/isy994", "requirements": [ "PyISY==1.1.2" ], diff --git a/homeassistant/components/itach/manifest.json b/homeassistant/components/itach/manifest.json index c26b19c636e59c..86bb362f8dc4b7 100644 --- a/homeassistant/components/itach/manifest.json +++ b/homeassistant/components/itach/manifest.json @@ -1,7 +1,7 @@ { "domain": "itach", "name": "Itach", - "documentation": "https://www.home-assistant.io/components/itach", + "documentation": "https://www.home-assistant.io/integrations/itach", "requirements": [ "pyitachip2ir==0.0.7" ], diff --git a/homeassistant/components/itunes/manifest.json b/homeassistant/components/itunes/manifest.json index 6f05125661e2c8..ec47deabc23ec5 100644 --- a/homeassistant/components/itunes/manifest.json +++ b/homeassistant/components/itunes/manifest.json @@ -1,7 +1,7 @@ { "domain": "itunes", "name": "Itunes", - "documentation": "https://www.home-assistant.io/components/itunes", + "documentation": "https://www.home-assistant.io/integrations/itunes", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index 2f6747ab4cc51d..a3aa3ad3fa8987 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -1,7 +1,7 @@ { "domain": "izone", "name": "izone", - "documentation": "https://www.home-assistant.io/components/izone", + "documentation": "https://www.home-assistant.io/integrations/izone", "requirements": [ "python-izone==1.1.1" ], "dependencies": [], "codeowners": [ "@Swamp-Ig" ], diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index fdc1d2943e6fe9..7b6653ba832141 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -1,7 +1,7 @@ { "domain": "jewish_calendar", "name": "Jewish calendar", - "documentation": "https://www.home-assistant.io/components/jewish_calendar", + "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "requirements": [ "hdate==0.9.0" ], diff --git a/homeassistant/components/joaoapps_join/manifest.json b/homeassistant/components/joaoapps_join/manifest.json index 220f2af2035552..a2c2e4b11b6378 100644 --- a/homeassistant/components/joaoapps_join/manifest.json +++ b/homeassistant/components/joaoapps_join/manifest.json @@ -1,7 +1,7 @@ { "domain": "joaoapps_join", "name": "Joaoapps join", - "documentation": "https://www.home-assistant.io/components/joaoapps_join", + "documentation": "https://www.home-assistant.io/integrations/joaoapps_join", "requirements": [ "python-join-api==0.0.4" ], diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index e65aab2b69da28..1ef84b74502a9f 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -1,7 +1,7 @@ { "domain": "juicenet", "name": "Juicenet", - "documentation": "https://www.home-assistant.io/components/juicenet", + "documentation": "https://www.home-assistant.io/integrations/juicenet", "requirements": [ "python-juicenet==0.0.5" ], diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json index 926f73fa4dbea9..eb3626a315be6e 100644 --- a/homeassistant/components/kaiterra/manifest.json +++ b/homeassistant/components/kaiterra/manifest.json @@ -1,7 +1,7 @@ { "domain": "kaiterra", "name": "Kaiterra", - "documentation": "https://www.home-assistant.io/components/kaiterra", + "documentation": "https://www.home-assistant.io/integrations/kaiterra", "requirements": ["kaiterra-async-client==0.0.2"], "codeowners": ["@Michsior14"], "dependencies": [] diff --git a/homeassistant/components/kankun/manifest.json b/homeassistant/components/kankun/manifest.json index 8e4e9747901e68..ef6bcbf92e2786 100644 --- a/homeassistant/components/kankun/manifest.json +++ b/homeassistant/components/kankun/manifest.json @@ -1,7 +1,7 @@ { "domain": "kankun", "name": "Kankun", - "documentation": "https://www.home-assistant.io/components/kankun", + "documentation": "https://www.home-assistant.io/integrations/kankun", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json index 9e959f35c9f32a..422a79cd0bedd6 100644 --- a/homeassistant/components/keba/manifest.json +++ b/homeassistant/components/keba/manifest.json @@ -1,7 +1,7 @@ { "domain": "keba", "name": "Keba Charging Station", - "documentation": "https://www.home-assistant.io/components/keba", + "documentation": "https://www.home-assistant.io/integrations/keba", "requirements": ["keba-kecontact==0.2.0"], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 417616161e56ea..41e45a9e5782f4 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -1,7 +1,7 @@ { "domain": "keenetic_ndms2", "name": "Keenetic ndms2", - "documentation": "https://www.home-assistant.io/components/keenetic_ndms2", + "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", "requirements": [ "ndms2_client==0.0.9" ], diff --git a/homeassistant/components/keyboard/manifest.json b/homeassistant/components/keyboard/manifest.json index 0e8ade339c2104..a3cf7a51ed7779 100644 --- a/homeassistant/components/keyboard/manifest.json +++ b/homeassistant/components/keyboard/manifest.json @@ -1,7 +1,7 @@ { "domain": "keyboard", "name": "Keyboard", - "documentation": "https://www.home-assistant.io/components/keyboard", + "documentation": "https://www.home-assistant.io/integrations/keyboard", "requirements": [ "pyuserinput==0.1.11" ], diff --git a/homeassistant/components/keyboard_remote/manifest.json b/homeassistant/components/keyboard_remote/manifest.json index d87d1abca4831d..6172de132bb8e1 100644 --- a/homeassistant/components/keyboard_remote/manifest.json +++ b/homeassistant/components/keyboard_remote/manifest.json @@ -1,7 +1,7 @@ { "domain": "keyboard_remote", "name": "Keyboard remote", - "documentation": "https://www.home-assistant.io/components/keyboard_remote", + "documentation": "https://www.home-assistant.io/integrations/keyboard_remote", "requirements": [ "evdev==0.6.1" ], diff --git a/homeassistant/components/kira/manifest.json b/homeassistant/components/kira/manifest.json index b7edd1f6c5f05d..78542bb7b66f86 100644 --- a/homeassistant/components/kira/manifest.json +++ b/homeassistant/components/kira/manifest.json @@ -1,7 +1,7 @@ { "domain": "kira", "name": "Kira", - "documentation": "https://www.home-assistant.io/components/kira", + "documentation": "https://www.home-assistant.io/integrations/kira", "requirements": [ "pykira==0.1.1" ], diff --git a/homeassistant/components/kiwi/manifest.json b/homeassistant/components/kiwi/manifest.json index 9f1595ebd77245..888c6533013f93 100644 --- a/homeassistant/components/kiwi/manifest.json +++ b/homeassistant/components/kiwi/manifest.json @@ -1,7 +1,7 @@ { "domain": "kiwi", "name": "Kiwi", - "documentation": "https://www.home-assistant.io/components/kiwi", + "documentation": "https://www.home-assistant.io/integrations/kiwi", "requirements": [ "kiwiki-client==0.1.1" ], diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 3b2d1414034f4f..76f15f3bdb8efd 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -1,7 +1,7 @@ { "domain": "knx", "name": "Knx", - "documentation": "https://www.home-assistant.io/components/knx", + "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ "xknx==0.11.1" ], diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 8c684d495e91db..ef138bb2ee26ee 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -1,7 +1,7 @@ { "domain": "kodi", "name": "Kodi", - "documentation": "https://www.home-assistant.io/components/kodi", + "documentation": "https://www.home-assistant.io/integrations/kodi", "requirements": [ "jsonrpc-async==0.6", "jsonrpc-websocket==0.6" diff --git a/homeassistant/components/konnected/manifest.json b/homeassistant/components/konnected/manifest.json index e4129af39bd10a..397373499aef67 100644 --- a/homeassistant/components/konnected/manifest.json +++ b/homeassistant/components/konnected/manifest.json @@ -1,7 +1,7 @@ { "domain": "konnected", "name": "Konnected", - "documentation": "https://www.home-assistant.io/components/konnected", + "documentation": "https://www.home-assistant.io/integrations/konnected", "requirements": [ "konnected==0.1.5" ], diff --git a/homeassistant/components/kwb/manifest.json b/homeassistant/components/kwb/manifest.json index 783907c02202e4..f79b0f5b35254d 100644 --- a/homeassistant/components/kwb/manifest.json +++ b/homeassistant/components/kwb/manifest.json @@ -1,7 +1,7 @@ { "domain": "kwb", "name": "Kwb", - "documentation": "https://www.home-assistant.io/components/kwb", + "documentation": "https://www.home-assistant.io/integrations/kwb", "requirements": [ "pykwb==0.0.8" ], diff --git a/homeassistant/components/lacrosse/manifest.json b/homeassistant/components/lacrosse/manifest.json index 99dd4889213361..c30a147342b36c 100644 --- a/homeassistant/components/lacrosse/manifest.json +++ b/homeassistant/components/lacrosse/manifest.json @@ -1,7 +1,7 @@ { "domain": "lacrosse", "name": "Lacrosse", - "documentation": "https://www.home-assistant.io/components/lacrosse", + "documentation": "https://www.home-assistant.io/integrations/lacrosse", "requirements": [ "pylacrosse==0.4.0" ], diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index bbf22918a75545..72e12e78ba4b92 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -1,7 +1,7 @@ { "domain": "lametric", "name": "Lametric", - "documentation": "https://www.home-assistant.io/components/lametric", + "documentation": "https://www.home-assistant.io/integrations/lametric", "requirements": [ "lmnotify==0.0.4" ], diff --git a/homeassistant/components/lannouncer/manifest.json b/homeassistant/components/lannouncer/manifest.json index 951dd3ff85b5e2..47bdd1ee0aef21 100644 --- a/homeassistant/components/lannouncer/manifest.json +++ b/homeassistant/components/lannouncer/manifest.json @@ -1,7 +1,7 @@ { "domain": "lannouncer", "name": "Lannouncer", - "documentation": "https://www.home-assistant.io/components/lannouncer", + "documentation": "https://www.home-assistant.io/integrations/lannouncer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index 2617b3e206bea8..78ecfd4efb07de 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -1,7 +1,7 @@ { "domain": "lastfm", "name": "Lastfm", - "documentation": "https://www.home-assistant.io/components/lastfm", + "documentation": "https://www.home-assistant.io/integrations/lastfm", "requirements": [ "pylast==3.1.0" ], diff --git a/homeassistant/components/launch_library/manifest.json b/homeassistant/components/launch_library/manifest.json index bbe9fa8ad054ce..5bf63f2b099044 100644 --- a/homeassistant/components/launch_library/manifest.json +++ b/homeassistant/components/launch_library/manifest.json @@ -1,7 +1,7 @@ { "domain": "launch_library", "name": "Launch library", - "documentation": "https://www.home-assistant.io/components/launch_library", + "documentation": "https://www.home-assistant.io/integrations/launch_library", "requirements": [ "pylaunches==0.2.0" ], diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 5a85b6673f2471..dcafe908d5cb28 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -1,7 +1,7 @@ { "domain": "lcn", "name": "Lcn", - "documentation": "https://www.home-assistant.io/components/lcn", + "documentation": "https://www.home-assistant.io/integrations/lcn", "requirements": [ "pypck==0.6.3" ], diff --git a/homeassistant/components/lg_netcast/manifest.json b/homeassistant/components/lg_netcast/manifest.json index 1728aa50614656..3f3d9d85c52091 100644 --- a/homeassistant/components/lg_netcast/manifest.json +++ b/homeassistant/components/lg_netcast/manifest.json @@ -1,7 +1,7 @@ { "domain": "lg_netcast", "name": "Lg netcast", - "documentation": "https://www.home-assistant.io/components/lg_netcast", + "documentation": "https://www.home-assistant.io/integrations/lg_netcast", "requirements": [ "pylgnetcast-homeassistant==0.2.0.dev0" ], diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index b09c8809382a7d..603f755fac11c8 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -1,7 +1,7 @@ { "domain": "lg_soundbar", "name": "Lg soundbar", - "documentation": "https://www.home-assistant.io/components/lg_soundbar", + "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", "requirements": [ "temescal==0.1" ], diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 9eae371070a5b9..a890ec39375788 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -2,7 +2,7 @@ "domain": "life360", "name": "Life360", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/life360", + "documentation": "https://www.home-assistant.io/integrations/life360", "dependencies": [], "codeowners": [ "@pnbruckner" diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 131d1a23b6a5f3..a8c2755aefb5ad 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -2,7 +2,7 @@ "domain": "lifx", "name": "Lifx", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/lifx", + "documentation": "https://www.home-assistant.io/integrations/lifx", "requirements": [ "aiolifx==0.6.7", "aiolifx_effects==0.2.2" diff --git a/homeassistant/components/lifx_cloud/manifest.json b/homeassistant/components/lifx_cloud/manifest.json index 83805692e4d246..f6aa8ccb7c5b86 100644 --- a/homeassistant/components/lifx_cloud/manifest.json +++ b/homeassistant/components/lifx_cloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "lifx_cloud", "name": "Lifx cloud", - "documentation": "https://www.home-assistant.io/components/lifx_cloud", + "documentation": "https://www.home-assistant.io/integrations/lifx_cloud", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/lifx_legacy/manifest.json b/homeassistant/components/lifx_legacy/manifest.json index fb38b41f314c4a..de5f5ff04dee4f 100644 --- a/homeassistant/components/lifx_legacy/manifest.json +++ b/homeassistant/components/lifx_legacy/manifest.json @@ -1,7 +1,7 @@ { "domain": "lifx_legacy", "name": "Lifx legacy", - "documentation": "https://www.home-assistant.io/components/lifx_legacy", + "documentation": "https://www.home-assistant.io/integrations/lifx_legacy", "requirements": [ "liffylights==0.9.4" ], diff --git a/homeassistant/components/light/manifest.json b/homeassistant/components/light/manifest.json index 62eb96967f5ec5..88e7585f802b8a 100644 --- a/homeassistant/components/light/manifest.json +++ b/homeassistant/components/light/manifest.json @@ -1,7 +1,7 @@ { "domain": "light", "name": "Light", - "documentation": "https://www.home-assistant.io/components/light", + "documentation": "https://www.home-assistant.io/integrations/light", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/lightwave/manifest.json b/homeassistant/components/lightwave/manifest.json index a26500f69a6e25..4b2456f0df5814 100644 --- a/homeassistant/components/lightwave/manifest.json +++ b/homeassistant/components/lightwave/manifest.json @@ -1,7 +1,7 @@ { "domain": "lightwave", "name": "Lightwave", - "documentation": "https://www.home-assistant.io/components/lightwave", + "documentation": "https://www.home-assistant.io/integrations/lightwave", "requirements": [ "lightwave==0.15" ], diff --git a/homeassistant/components/limitlessled/manifest.json b/homeassistant/components/limitlessled/manifest.json index f8b42fabcbe12b..5eff655e806c2f 100644 --- a/homeassistant/components/limitlessled/manifest.json +++ b/homeassistant/components/limitlessled/manifest.json @@ -1,7 +1,7 @@ { "domain": "limitlessled", "name": "Limitlessled", - "documentation": "https://www.home-assistant.io/components/limitlessled", + "documentation": "https://www.home-assistant.io/integrations/limitlessled", "requirements": [ "limitlessled==1.1.3" ], diff --git a/homeassistant/components/linksys_smart/manifest.json b/homeassistant/components/linksys_smart/manifest.json index 19bb079c29cef3..28ed3f52036b4d 100644 --- a/homeassistant/components/linksys_smart/manifest.json +++ b/homeassistant/components/linksys_smart/manifest.json @@ -1,7 +1,7 @@ { "domain": "linksys_smart", "name": "Linksys smart", - "documentation": "https://www.home-assistant.io/components/linksys_smart", + "documentation": "https://www.home-assistant.io/integrations/linksys_smart", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/linky/manifest.json b/homeassistant/components/linky/manifest.json index 10a5bbcf86498c..a2505427f450d0 100644 --- a/homeassistant/components/linky/manifest.json +++ b/homeassistant/components/linky/manifest.json @@ -2,7 +2,7 @@ "domain": "linky", "name": "Linky", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/linky", + "documentation": "https://www.home-assistant.io/integrations/linky", "requirements": [ "pylinky==0.4.0" ], diff --git a/homeassistant/components/linode/manifest.json b/homeassistant/components/linode/manifest.json index 7dc2e0d7518e69..064f7e1ccf07dc 100644 --- a/homeassistant/components/linode/manifest.json +++ b/homeassistant/components/linode/manifest.json @@ -1,7 +1,7 @@ { "domain": "linode", "name": "Linode", - "documentation": "https://www.home-assistant.io/components/linode", + "documentation": "https://www.home-assistant.io/integrations/linode", "requirements": [ "linode-api==4.1.9b1" ], diff --git a/homeassistant/components/linux_battery/manifest.json b/homeassistant/components/linux_battery/manifest.json index 4c32b88b2d5b7e..3730f01622fd81 100644 --- a/homeassistant/components/linux_battery/manifest.json +++ b/homeassistant/components/linux_battery/manifest.json @@ -1,7 +1,7 @@ { "domain": "linux_battery", "name": "Linux battery", - "documentation": "https://www.home-assistant.io/components/linux_battery", + "documentation": "https://www.home-assistant.io/integrations/linux_battery", "requirements": [ "batinfo==0.4.2" ], diff --git a/homeassistant/components/lirc/manifest.json b/homeassistant/components/lirc/manifest.json index d11cf0b2f1ef71..b15799b54e330d 100644 --- a/homeassistant/components/lirc/manifest.json +++ b/homeassistant/components/lirc/manifest.json @@ -1,7 +1,7 @@ { "domain": "lirc", "name": "Lirc", - "documentation": "https://www.home-assistant.io/components/lirc", + "documentation": "https://www.home-assistant.io/integrations/lirc", "requirements": [ "python-lirc==1.2.3" ], diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json index 08bcac67903088..988e2bd1ed47b0 100644 --- a/homeassistant/components/litejet/manifest.json +++ b/homeassistant/components/litejet/manifest.json @@ -1,7 +1,7 @@ { "domain": "litejet", "name": "Litejet", - "documentation": "https://www.home-assistant.io/components/litejet", + "documentation": "https://www.home-assistant.io/integrations/litejet", "requirements": [ "pylitejet==0.1" ], diff --git a/homeassistant/components/liveboxplaytv/manifest.json b/homeassistant/components/liveboxplaytv/manifest.json index 3393022a363d62..bcb2b53f081af6 100644 --- a/homeassistant/components/liveboxplaytv/manifest.json +++ b/homeassistant/components/liveboxplaytv/manifest.json @@ -1,7 +1,7 @@ { "domain": "liveboxplaytv", "name": "Liveboxplaytv", - "documentation": "https://www.home-assistant.io/components/liveboxplaytv", + "documentation": "https://www.home-assistant.io/integrations/liveboxplaytv", "requirements": [ "liveboxplaytv==2.0.2", "pyteleloisirs==3.5" diff --git a/homeassistant/components/llamalab_automate/manifest.json b/homeassistant/components/llamalab_automate/manifest.json index e66050fceb5728..2f46e6e790c177 100644 --- a/homeassistant/components/llamalab_automate/manifest.json +++ b/homeassistant/components/llamalab_automate/manifest.json @@ -1,7 +1,7 @@ { "domain": "llamalab_automate", "name": "Llamalab automate", - "documentation": "https://www.home-assistant.io/components/llamalab_automate", + "documentation": "https://www.home-assistant.io/integrations/llamalab_automate", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/local_file/manifest.json b/homeassistant/components/local_file/manifest.json index 14a503f33f571d..ded748be8dc5ac 100644 --- a/homeassistant/components/local_file/manifest.json +++ b/homeassistant/components/local_file/manifest.json @@ -1,7 +1,7 @@ { "domain": "local_file", "name": "Local file", - "documentation": "https://www.home-assistant.io/components/local_file", + "documentation": "https://www.home-assistant.io/integrations/local_file", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/locative/manifest.json b/homeassistant/components/locative/manifest.json index be2eb07a23cb1b..46a2d4de20e022 100644 --- a/homeassistant/components/locative/manifest.json +++ b/homeassistant/components/locative/manifest.json @@ -2,7 +2,7 @@ "domain": "locative", "name": "Locative", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/locative", + "documentation": "https://www.home-assistant.io/integrations/locative", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/lock/manifest.json b/homeassistant/components/lock/manifest.json index 29a7a5513d0854..0a76628a5b5211 100644 --- a/homeassistant/components/lock/manifest.json +++ b/homeassistant/components/lock/manifest.json @@ -1,7 +1,7 @@ { "domain": "lock", "name": "Lock", - "documentation": "https://www.home-assistant.io/components/lock", + "documentation": "https://www.home-assistant.io/integrations/lock", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/lockitron/manifest.json b/homeassistant/components/lockitron/manifest.json index b515d65a14fda4..18ab9036c5ec03 100644 --- a/homeassistant/components/lockitron/manifest.json +++ b/homeassistant/components/lockitron/manifest.json @@ -1,7 +1,7 @@ { "domain": "lockitron", "name": "Lockitron", - "documentation": "https://www.home-assistant.io/components/lockitron", + "documentation": "https://www.home-assistant.io/integrations/lockitron", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index cedce8152a294b..e8e3ad8ac2e580 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -1,7 +1,7 @@ { "domain": "logbook", "name": "Logbook", - "documentation": "https://www.home-assistant.io/components/logbook", + "documentation": "https://www.home-assistant.io/integrations/logbook", "requirements": [], "dependencies": [ "frontend", diff --git a/homeassistant/components/logentries/manifest.json b/homeassistant/components/logentries/manifest.json index 60be8f275eef60..c546030853faef 100644 --- a/homeassistant/components/logentries/manifest.json +++ b/homeassistant/components/logentries/manifest.json @@ -1,7 +1,7 @@ { "domain": "logentries", "name": "Logentries", - "documentation": "https://www.home-assistant.io/components/logentries", + "documentation": "https://www.home-assistant.io/integrations/logentries", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/logger/manifest.json b/homeassistant/components/logger/manifest.json index c6b6238703982d..ac201fb00a1d62 100644 --- a/homeassistant/components/logger/manifest.json +++ b/homeassistant/components/logger/manifest.json @@ -1,7 +1,7 @@ { "domain": "logger", "name": "Logger", - "documentation": "https://www.home-assistant.io/components/logger", + "documentation": "https://www.home-assistant.io/integrations/logger", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index b1767748395206..22502956e06aa5 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -2,7 +2,7 @@ "domain": "logi_circle", "name": "Logi Circle", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/logi_circle", + "documentation": "https://www.home-assistant.io/integrations/logi_circle", "requirements": ["logi_circle==0.2.2"], "dependencies": ["ffmpeg"], "codeowners": ["@evanjd"] diff --git a/homeassistant/components/london_air/manifest.json b/homeassistant/components/london_air/manifest.json index 3f0c97edfe012b..cca4a54bda8bd2 100644 --- a/homeassistant/components/london_air/manifest.json +++ b/homeassistant/components/london_air/manifest.json @@ -1,7 +1,7 @@ { "domain": "london_air", "name": "London air", - "documentation": "https://www.home-assistant.io/components/london_air", + "documentation": "https://www.home-assistant.io/integrations/london_air", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/london_underground/manifest.json b/homeassistant/components/london_underground/manifest.json index 5262fa4837ea9a..e66d5c1820dcdc 100644 --- a/homeassistant/components/london_underground/manifest.json +++ b/homeassistant/components/london_underground/manifest.json @@ -1,7 +1,7 @@ { "domain": "london_underground", "name": "London underground", - "documentation": "https://www.home-assistant.io/components/london_underground", + "documentation": "https://www.home-assistant.io/integrations/london_underground", "requirements": [ "london-tube-status==0.2" ], diff --git a/homeassistant/components/loopenergy/manifest.json b/homeassistant/components/loopenergy/manifest.json index 20fe6fac2aa998..41e3d0dd6b07cc 100644 --- a/homeassistant/components/loopenergy/manifest.json +++ b/homeassistant/components/loopenergy/manifest.json @@ -1,7 +1,7 @@ { "domain": "loopenergy", "name": "Loopenergy", - "documentation": "https://www.home-assistant.io/components/loopenergy", + "documentation": "https://www.home-assistant.io/integrations/loopenergy", "requirements": [ "pyloopenergy==0.1.3" ], diff --git a/homeassistant/components/lovelace/manifest.json b/homeassistant/components/lovelace/manifest.json index dd8da40efe41a9..72be440d54c0e2 100644 --- a/homeassistant/components/lovelace/manifest.json +++ b/homeassistant/components/lovelace/manifest.json @@ -1,7 +1,7 @@ { "domain": "lovelace", "name": "Lovelace", - "documentation": "https://www.home-assistant.io/components/lovelace", + "documentation": "https://www.home-assistant.io/integrations/lovelace", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index dffb4b52667369..646fc1a3cbfa50 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -1,7 +1,7 @@ { "domain": "luci", "name": "Luci", - "documentation": "https://www.home-assistant.io/components/luci", + "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": [ "openwrt-luci-rpc==1.1.1" ], diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index 26d6c21f3a95df..112b58ba65df16 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -2,7 +2,7 @@ "domain": "luftdaten", "name": "Luftdaten", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/luftdaten", + "documentation": "https://www.home-assistant.io/integrations/luftdaten", "requirements": [ "luftdaten==0.6.3" ], diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index 344ec82d976cb0..bb6b18243ec13f 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -1,7 +1,7 @@ { "domain": "lupusec", "name": "Lupusec", - "documentation": "https://www.home-assistant.io/components/lupusec", + "documentation": "https://www.home-assistant.io/integrations/lupusec", "requirements": [ "lupupy==0.0.17" ], diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index 451a6f3e33d822..cace2770de0bea 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -1,7 +1,7 @@ { "domain": "lutron", "name": "Lutron", - "documentation": "https://www.home-assistant.io/components/lutron", + "documentation": "https://www.home-assistant.io/integrations/lutron", "requirements": [ "pylutron==0.2.5" ], diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 4da58cdfc40274..d1501a562db4bd 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -1,7 +1,7 @@ { "domain": "lutron_caseta", "name": "Lutron caseta", - "documentation": "https://www.home-assistant.io/components/lutron_caseta", + "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", "requirements": [ "pylutron-caseta==0.5.0" ], diff --git a/homeassistant/components/lw12wifi/manifest.json b/homeassistant/components/lw12wifi/manifest.json index 205072055bbe22..7f830cda25becc 100644 --- a/homeassistant/components/lw12wifi/manifest.json +++ b/homeassistant/components/lw12wifi/manifest.json @@ -1,7 +1,7 @@ { "domain": "lw12wifi", "name": "Lw12wifi", - "documentation": "https://www.home-assistant.io/components/lw12wifi", + "documentation": "https://www.home-assistant.io/integrations/lw12wifi", "requirements": [ "lw12==0.9.2" ], diff --git a/homeassistant/components/lyft/manifest.json b/homeassistant/components/lyft/manifest.json index ff7da7190d94e6..e8b593b31453d1 100644 --- a/homeassistant/components/lyft/manifest.json +++ b/homeassistant/components/lyft/manifest.json @@ -1,7 +1,7 @@ { "domain": "lyft", "name": "Lyft", - "documentation": "https://www.home-assistant.io/components/lyft", + "documentation": "https://www.home-assistant.io/integrations/lyft", "requirements": [ "lyft_rides==0.2" ], diff --git a/homeassistant/components/magicseaweed/manifest.json b/homeassistant/components/magicseaweed/manifest.json index 6534d927f1b872..41795c117a9fa6 100644 --- a/homeassistant/components/magicseaweed/manifest.json +++ b/homeassistant/components/magicseaweed/manifest.json @@ -1,7 +1,7 @@ { "domain": "magicseaweed", "name": "Magicseaweed", - "documentation": "https://www.home-assistant.io/components/magicseaweed", + "documentation": "https://www.home-assistant.io/integrations/magicseaweed", "requirements": [ "magicseaweed==1.0.3" ], diff --git a/homeassistant/components/mailbox/manifest.json b/homeassistant/components/mailbox/manifest.json index 4ca1db564a4cf0..2883c9caf3429d 100644 --- a/homeassistant/components/mailbox/manifest.json +++ b/homeassistant/components/mailbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "mailbox", "name": "Mailbox", - "documentation": "https://www.home-assistant.io/components/mailbox", + "documentation": "https://www.home-assistant.io/integrations/mailbox", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/mailgun/manifest.json b/homeassistant/components/mailgun/manifest.json index 9ed7a50a8e3c10..c0bd0823b8fe20 100644 --- a/homeassistant/components/mailgun/manifest.json +++ b/homeassistant/components/mailgun/manifest.json @@ -2,7 +2,7 @@ "domain": "mailgun", "name": "Mailgun", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/mailgun", + "documentation": "https://www.home-assistant.io/integrations/mailgun", "requirements": [ "pymailgunner==1.4" ], diff --git a/homeassistant/components/manual/manifest.json b/homeassistant/components/manual/manifest.json index 6c788971629ea8..12d5297c010167 100644 --- a/homeassistant/components/manual/manifest.json +++ b/homeassistant/components/manual/manifest.json @@ -1,7 +1,7 @@ { "domain": "manual", "name": "Manual", - "documentation": "https://www.home-assistant.io/components/manual", + "documentation": "https://www.home-assistant.io/integrations/manual", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/manual_mqtt/manifest.json b/homeassistant/components/manual_mqtt/manifest.json index 81cd1338450fd9..d9ec004e6a82dc 100644 --- a/homeassistant/components/manual_mqtt/manifest.json +++ b/homeassistant/components/manual_mqtt/manifest.json @@ -1,7 +1,7 @@ { "domain": "manual_mqtt", "name": "Manual mqtt", - "documentation": "https://www.home-assistant.io/components/manual_mqtt", + "documentation": "https://www.home-assistant.io/integrations/manual_mqtt", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/map/manifest.json b/homeassistant/components/map/manifest.json index d26d7d9530fdda..e64d18c92e1d5c 100644 --- a/homeassistant/components/map/manifest.json +++ b/homeassistant/components/map/manifest.json @@ -1,7 +1,7 @@ { "domain": "map", "name": "Map", - "documentation": "https://www.home-assistant.io/components/map", + "documentation": "https://www.home-assistant.io/integrations/map", "requirements": [], "dependencies": [ "frontend" diff --git a/homeassistant/components/marytts/manifest.json b/homeassistant/components/marytts/manifest.json index 5316935c442db6..0188f405e1492e 100644 --- a/homeassistant/components/marytts/manifest.json +++ b/homeassistant/components/marytts/manifest.json @@ -1,7 +1,7 @@ { "domain": "marytts", "name": "Marytts", - "documentation": "https://www.home-assistant.io/components/marytts", + "documentation": "https://www.home-assistant.io/integrations/marytts", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index 4005e51e373505..e041ba2f669a51 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -1,7 +1,7 @@ { "domain": "mastodon", "name": "Mastodon", - "documentation": "https://www.home-assistant.io/components/mastodon", + "documentation": "https://www.home-assistant.io/integrations/mastodon", "requirements": [ "Mastodon.py==1.4.6" ], diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 9ea1a6f0c5558c..a467518c04ef87 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -1,7 +1,7 @@ { "domain": "matrix", "name": "Matrix", - "documentation": "https://www.home-assistant.io/components/matrix", + "documentation": "https://www.home-assistant.io/integrations/matrix", "requirements": [ "matrix-client==0.2.0" ], diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index a28096c5eb7767..1f5b1eef93592d 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -1,7 +1,7 @@ { "domain": "maxcube", "name": "Maxcube", - "documentation": "https://www.home-assistant.io/components/maxcube", + "documentation": "https://www.home-assistant.io/integrations/maxcube", "requirements": [ "maxcube-api==0.1.0" ], diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json index 41048683c9214a..2dbffd829f8b8a 100644 --- a/homeassistant/components/mcp23017/manifest.json +++ b/homeassistant/components/mcp23017/manifest.json @@ -1,7 +1,7 @@ { "domain": "mcp23017", "name": "MCP23017 I/O Expander", - "documentation": "https://www.home-assistant.io/components/mcp23017", + "documentation": "https://www.home-assistant.io/integrations/mcp23017", "requirements": [ "RPi.GPIO==0.6.5", "adafruit-blinka==1.2.1", diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 71e1a81135a662..886535555d5a84 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -1,7 +1,7 @@ { "domain": "media_extractor", "name": "Media extractor", - "documentation": "https://www.home-assistant.io/components/media_extractor", + "documentation": "https://www.home-assistant.io/integrations/media_extractor", "requirements": [ "youtube_dl==2019.09.28" ], diff --git a/homeassistant/components/media_player/manifest.json b/homeassistant/components/media_player/manifest.json index bf6f8fabafa43c..4df8ad8442a727 100644 --- a/homeassistant/components/media_player/manifest.json +++ b/homeassistant/components/media_player/manifest.json @@ -1,7 +1,7 @@ { "domain": "media_player", "name": "Media player", - "documentation": "https://www.home-assistant.io/components/media_player", + "documentation": "https://www.home-assistant.io/integrations/media_player", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/mediaroom/manifest.json b/homeassistant/components/mediaroom/manifest.json index 134d85fa1712d2..0e42cd7e535ad3 100644 --- a/homeassistant/components/mediaroom/manifest.json +++ b/homeassistant/components/mediaroom/manifest.json @@ -1,7 +1,7 @@ { "domain": "mediaroom", "name": "Mediaroom", - "documentation": "https://www.home-assistant.io/components/mediaroom", + "documentation": "https://www.home-assistant.io/integrations/mediaroom", "requirements": [ "pymediaroom==0.6.4" ], diff --git a/homeassistant/components/melissa/manifest.json b/homeassistant/components/melissa/manifest.json index f9fa1cab502cdb..64760338f35cd1 100644 --- a/homeassistant/components/melissa/manifest.json +++ b/homeassistant/components/melissa/manifest.json @@ -1,7 +1,7 @@ { "domain": "melissa", "name": "Melissa", - "documentation": "https://www.home-assistant.io/components/melissa", + "documentation": "https://www.home-assistant.io/integrations/melissa", "requirements": [ "py-melissa-climate==2.0.0" ], diff --git a/homeassistant/components/meraki/manifest.json b/homeassistant/components/meraki/manifest.json index d03679ed41ed4b..2add8663555a95 100644 --- a/homeassistant/components/meraki/manifest.json +++ b/homeassistant/components/meraki/manifest.json @@ -1,7 +1,7 @@ { "domain": "meraki", "name": "Meraki", - "documentation": "https://www.home-assistant.io/components/meraki", + "documentation": "https://www.home-assistant.io/integrations/meraki", "requirements": [], "dependencies": ["http"], "codeowners": [] diff --git a/homeassistant/components/message_bird/manifest.json b/homeassistant/components/message_bird/manifest.json index a6c49b3c39688f..79428f951f134d 100644 --- a/homeassistant/components/message_bird/manifest.json +++ b/homeassistant/components/message_bird/manifest.json @@ -1,7 +1,7 @@ { "domain": "message_bird", "name": "Message bird", - "documentation": "https://www.home-assistant.io/components/message_bird", + "documentation": "https://www.home-assistant.io/integrations/message_bird", "requirements": [ "messagebird==1.2.0" ], diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 426d0faf8608b7..2652e33b76cfae 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -2,7 +2,7 @@ "domain": "met", "name": "Met", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/met", + "documentation": "https://www.home-assistant.io/integrations/met", "requirements": [ "pyMetno==0.4.6" ], diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index b485458be409e2..ba043bf2a714f8 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -1,7 +1,7 @@ { "domain": "meteo_france", "name": "Meteo france", - "documentation": "https://www.home-assistant.io/components/meteo_france", + "documentation": "https://www.home-assistant.io/integrations/meteo_france", "requirements": [ "meteofrance==0.3.7", "vigilancemeteo==3.0.0" diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 692e55260850d3..ee14ce7d26d8d2 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -1,7 +1,7 @@ { "domain": "meteoalarm", "name": "meteoalarm", - "documentation": "https://www.home-assistant.io/components/meteoalarm", + "documentation": "https://www.home-assistant.io/integrations/meteoalarm", "requirements": [ "meteoalertapi==0.1.6" ], diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index f5d358854f6f79..f5624e33edbefd 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -1,7 +1,7 @@ { "domain": "metoffice", "name": "Metoffice", - "documentation": "https://www.home-assistant.io/components/metoffice", + "documentation": "https://www.home-assistant.io/integrations/metoffice", "requirements": [ "datapoint==0.4.3" ], diff --git a/homeassistant/components/mfi/manifest.json b/homeassistant/components/mfi/manifest.json index 1e84b39a366e4f..c08b4e4c88a577 100644 --- a/homeassistant/components/mfi/manifest.json +++ b/homeassistant/components/mfi/manifest.json @@ -1,7 +1,7 @@ { "domain": "mfi", "name": "Mfi", - "documentation": "https://www.home-assistant.io/components/mfi", + "documentation": "https://www.home-assistant.io/integrations/mfi", "requirements": [ "mficlient==0.3.0" ], diff --git a/homeassistant/components/mhz19/manifest.json b/homeassistant/components/mhz19/manifest.json index 8545db90e27583..5bffcf8e92c25c 100644 --- a/homeassistant/components/mhz19/manifest.json +++ b/homeassistant/components/mhz19/manifest.json @@ -1,7 +1,7 @@ { "domain": "mhz19", "name": "Mhz19", - "documentation": "https://www.home-assistant.io/components/mhz19", + "documentation": "https://www.home-assistant.io/integrations/mhz19", "requirements": [ "pmsensor==0.4" ], diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 827d961a093859..16ae94c212ef62 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -1,7 +1,7 @@ { "domain": "microsoft", "name": "Microsoft", - "documentation": "https://www.home-assistant.io/components/microsoft", + "documentation": "https://www.home-assistant.io/integrations/microsoft", "requirements": [ "pycsspeechtts==1.0.2" ], diff --git a/homeassistant/components/microsoft_face/manifest.json b/homeassistant/components/microsoft_face/manifest.json index 7f6c4fbd935759..1d51dca42cd214 100644 --- a/homeassistant/components/microsoft_face/manifest.json +++ b/homeassistant/components/microsoft_face/manifest.json @@ -1,7 +1,7 @@ { "domain": "microsoft_face", "name": "Microsoft face", - "documentation": "https://www.home-assistant.io/components/microsoft_face", + "documentation": "https://www.home-assistant.io/integrations/microsoft_face", "requirements": [], "dependencies": [ "camera" diff --git a/homeassistant/components/microsoft_face_detect/manifest.json b/homeassistant/components/microsoft_face_detect/manifest.json index b272a299cf5b93..12d73623e750d5 100644 --- a/homeassistant/components/microsoft_face_detect/manifest.json +++ b/homeassistant/components/microsoft_face_detect/manifest.json @@ -1,7 +1,7 @@ { "domain": "microsoft_face_detect", "name": "Microsoft face detect", - "documentation": "https://www.home-assistant.io/components/microsoft_face_detect", + "documentation": "https://www.home-assistant.io/integrations/microsoft_face_detect", "requirements": [], "dependencies": ["microsoft_face"], "codeowners": [] diff --git a/homeassistant/components/microsoft_face_identify/manifest.json b/homeassistant/components/microsoft_face_identify/manifest.json index 10e4bde103cfc9..a52aca1ac0a049 100644 --- a/homeassistant/components/microsoft_face_identify/manifest.json +++ b/homeassistant/components/microsoft_face_identify/manifest.json @@ -1,7 +1,7 @@ { "domain": "microsoft_face_identify", "name": "Microsoft face identify", - "documentation": "https://www.home-assistant.io/components/microsoft_face_identify", + "documentation": "https://www.home-assistant.io/integrations/microsoft_face_identify", "requirements": [], "dependencies": ["microsoft_face"], "codeowners": [] diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index c7ef2b89611c43..54fa59135b338a 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -1,7 +1,7 @@ { "domain": "miflora", "name": "Miflora", - "documentation": "https://www.home-assistant.io/components/miflora", + "documentation": "https://www.home-assistant.io/integrations/miflora", "requirements": [ "bluepy==1.1.4", "miflora==0.4.0" diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index 92869856545ca6..9a05f5a9f870e0 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -1,7 +1,7 @@ { "domain": "mikrotik", "name": "Mikrotik", - "documentation": "https://www.home-assistant.io/components/mikrotik", + "documentation": "https://www.home-assistant.io/integrations/mikrotik", "requirements": [ "librouteros==2.3.0" ], diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 05efb845c12ed5..85e70c78ede6eb 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -1,7 +1,7 @@ { "domain": "mill", "name": "Mill", - "documentation": "https://www.home-assistant.io/components/mill", + "documentation": "https://www.home-assistant.io/integrations/mill", "requirements": [ "millheater==0.3.4" ], diff --git a/homeassistant/components/min_max/manifest.json b/homeassistant/components/min_max/manifest.json index ea6befe498b42e..ed899a0438fac0 100644 --- a/homeassistant/components/min_max/manifest.json +++ b/homeassistant/components/min_max/manifest.json @@ -1,7 +1,7 @@ { "domain": "min_max", "name": "Min max", - "documentation": "https://www.home-assistant.io/components/min_max", + "documentation": "https://www.home-assistant.io/integrations/min_max", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json index 2b2f84836ead39..bc373633503819 100644 --- a/homeassistant/components/minio/manifest.json +++ b/homeassistant/components/minio/manifest.json @@ -1,7 +1,7 @@ { "domain": "minio", "name": "Minio", - "documentation": "https://www.home-assistant.io/components/minio", + "documentation": "https://www.home-assistant.io/integrations/minio", "requirements": [ "minio==4.0.9" ], diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index 2324a861b38e5d..612e7c19f8bcd9 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -1,7 +1,7 @@ { "domain": "mitemp_bt", "name": "Mitemp bt", - "documentation": "https://www.home-assistant.io/components/mitemp_bt", + "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", "requirements": [ "mitemp_bt==0.0.1" ], diff --git a/homeassistant/components/mjpeg/manifest.json b/homeassistant/components/mjpeg/manifest.json index 2ecd66910be6c0..93f01ac2e3ab23 100644 --- a/homeassistant/components/mjpeg/manifest.json +++ b/homeassistant/components/mjpeg/manifest.json @@ -1,7 +1,7 @@ { "domain": "mjpeg", "name": "Mjpeg", - "documentation": "https://www.home-assistant.io/components/mjpeg", + "documentation": "https://www.home-assistant.io/integrations/mjpeg", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 85c6231daa8839..8c95ca4ad41e49 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -2,7 +2,7 @@ "domain": "mobile_app", "name": "Home Assistant Mobile App Support", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/mobile_app", + "documentation": "https://www.home-assistant.io/integrations/mobile_app", "requirements": [ "PyNaCl==1.3.0" ], diff --git a/homeassistant/components/mochad/manifest.json b/homeassistant/components/mochad/manifest.json index 0e5c4dd1ff3c1b..8994223fe31443 100644 --- a/homeassistant/components/mochad/manifest.json +++ b/homeassistant/components/mochad/manifest.json @@ -1,7 +1,7 @@ { "domain": "mochad", "name": "Mochad", - "documentation": "https://www.home-assistant.io/components/mochad", + "documentation": "https://www.home-assistant.io/integrations/mochad", "requirements": [ "pymochad==0.2.0" ], diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index e27f594b0af020..8d271d5a95f106 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -1,7 +1,7 @@ { "domain": "modbus", "name": "Modbus", - "documentation": "https://www.home-assistant.io/components/modbus", + "documentation": "https://www.home-assistant.io/integrations/modbus", "requirements": [ "pymodbus==1.5.2" ], diff --git a/homeassistant/components/modem_callerid/manifest.json b/homeassistant/components/modem_callerid/manifest.json index e3d6d19b803ddf..80174b1a83a90e 100644 --- a/homeassistant/components/modem_callerid/manifest.json +++ b/homeassistant/components/modem_callerid/manifest.json @@ -1,7 +1,7 @@ { "domain": "modem_callerid", "name": "Modem callerid", - "documentation": "https://www.home-assistant.io/components/modem_callerid", + "documentation": "https://www.home-assistant.io/integrations/modem_callerid", "requirements": [ "basicmodem==0.7" ], diff --git a/homeassistant/components/mold_indicator/manifest.json b/homeassistant/components/mold_indicator/manifest.json index de4680927a4737..1205b53ccaf1f0 100644 --- a/homeassistant/components/mold_indicator/manifest.json +++ b/homeassistant/components/mold_indicator/manifest.json @@ -1,7 +1,7 @@ { "domain": "mold_indicator", "name": "Mold indicator", - "documentation": "https://www.home-assistant.io/components/mold_indicator", + "documentation": "https://www.home-assistant.io/integrations/mold_indicator", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/monoprice/manifest.json b/homeassistant/components/monoprice/manifest.json index aa07911a697304..47db73ce0de834 100644 --- a/homeassistant/components/monoprice/manifest.json +++ b/homeassistant/components/monoprice/manifest.json @@ -1,7 +1,7 @@ { "domain": "monoprice", "name": "Monoprice", - "documentation": "https://www.home-assistant.io/components/monoprice", + "documentation": "https://www.home-assistant.io/integrations/monoprice", "requirements": [ "pymonoprice==0.3" ], diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 50a93fce20a50c..56b5a1b8181239 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -1,7 +1,7 @@ { "domain": "moon", "name": "Moon", - "documentation": "https://www.home-assistant.io/components/moon", + "documentation": "https://www.home-assistant.io/integrations/moon", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/mopar/manifest.json b/homeassistant/components/mopar/manifest.json index 5acd5bbdcdbbbd..ddb51c9e682a59 100644 --- a/homeassistant/components/mopar/manifest.json +++ b/homeassistant/components/mopar/manifest.json @@ -1,7 +1,7 @@ { "domain": "mopar", "name": "Mopar", - "documentation": "https://www.home-assistant.io/components/mopar", + "documentation": "https://www.home-assistant.io/integrations/mopar", "requirements": [ "motorparts==1.1.0" ], diff --git a/homeassistant/components/mpchc/manifest.json b/homeassistant/components/mpchc/manifest.json index e874ca288912b7..7d4192434721c6 100644 --- a/homeassistant/components/mpchc/manifest.json +++ b/homeassistant/components/mpchc/manifest.json @@ -1,7 +1,7 @@ { "domain": "mpchc", "name": "Mpchc", - "documentation": "https://www.home-assistant.io/components/mpchc", + "documentation": "https://www.home-assistant.io/integrations/mpchc", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index beee3137ef544e..b7440f655dbb1f 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -1,7 +1,7 @@ { "domain": "mpd", "name": "Mpd", - "documentation": "https://www.home-assistant.io/components/mpd", + "documentation": "https://www.home-assistant.io/integrations/mpd", "requirements": [ "python-mpd2==1.0.0" ], diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index 2df50699a9d8d3..c1b6659e1c0733 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -2,7 +2,7 @@ "domain": "mqtt", "name": "MQTT", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/mqtt", + "documentation": "https://www.home-assistant.io/integrations/mqtt", "requirements": [ "hbmqtt==0.9.5", "paho-mqtt==1.4.0" diff --git a/homeassistant/components/mqtt_eventstream/manifest.json b/homeassistant/components/mqtt_eventstream/manifest.json index e795c8aaf181d8..0d36cd6616d148 100644 --- a/homeassistant/components/mqtt_eventstream/manifest.json +++ b/homeassistant/components/mqtt_eventstream/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt_eventstream", "name": "Mqtt eventstream", - "documentation": "https://www.home-assistant.io/components/mqtt_eventstream", + "documentation": "https://www.home-assistant.io/integrations/mqtt_eventstream", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/mqtt_json/manifest.json b/homeassistant/components/mqtt_json/manifest.json index a1986b2bf2eee2..b54488397941c1 100644 --- a/homeassistant/components/mqtt_json/manifest.json +++ b/homeassistant/components/mqtt_json/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt_json", "name": "Mqtt json", - "documentation": "https://www.home-assistant.io/components/mqtt_json", + "documentation": "https://www.home-assistant.io/integrations/mqtt_json", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/mqtt_room/manifest.json b/homeassistant/components/mqtt_room/manifest.json index 8fc90b0bcb1837..93450b001638fd 100644 --- a/homeassistant/components/mqtt_room/manifest.json +++ b/homeassistant/components/mqtt_room/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt_room", "name": "Mqtt room", - "documentation": "https://www.home-assistant.io/components/mqtt_room", + "documentation": "https://www.home-assistant.io/integrations/mqtt_room", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/mqtt_statestream/manifest.json b/homeassistant/components/mqtt_statestream/manifest.json index 5fa9936372932d..840a53591a8235 100644 --- a/homeassistant/components/mqtt_statestream/manifest.json +++ b/homeassistant/components/mqtt_statestream/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt_statestream", "name": "Mqtt statestream", - "documentation": "https://www.home-assistant.io/components/mqtt_statestream", + "documentation": "https://www.home-assistant.io/integrations/mqtt_statestream", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/mvglive/manifest.json b/homeassistant/components/mvglive/manifest.json index 5626e2444849fb..e47b7c07d8bc5f 100644 --- a/homeassistant/components/mvglive/manifest.json +++ b/homeassistant/components/mvglive/manifest.json @@ -1,7 +1,7 @@ { "domain": "mvglive", "name": "Mvglive", - "documentation": "https://www.home-assistant.io/components/mvglive", + "documentation": "https://www.home-assistant.io/integrations/mvglive", "requirements": [ "PyMVGLive==1.1.4" ], diff --git a/homeassistant/components/mychevy/manifest.json b/homeassistant/components/mychevy/manifest.json index 1ff997372ed214..20933c9b2fcf0b 100644 --- a/homeassistant/components/mychevy/manifest.json +++ b/homeassistant/components/mychevy/manifest.json @@ -1,7 +1,7 @@ { "domain": "mychevy", "name": "Mychevy", - "documentation": "https://www.home-assistant.io/components/mychevy", + "documentation": "https://www.home-assistant.io/integrations/mychevy", "requirements": [ "mychevy==1.2.0" ], diff --git a/homeassistant/components/mycroft/manifest.json b/homeassistant/components/mycroft/manifest.json index 77e5a524aacc47..5d5ee195381568 100644 --- a/homeassistant/components/mycroft/manifest.json +++ b/homeassistant/components/mycroft/manifest.json @@ -1,7 +1,7 @@ { "domain": "mycroft", "name": "Mycroft", - "documentation": "https://www.home-assistant.io/components/mycroft", + "documentation": "https://www.home-assistant.io/integrations/mycroft", "requirements": [ "mycroftapi==2.0" ], diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index b870ff663098f6..213679b320a632 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -1,7 +1,7 @@ { "domain": "myq", "name": "Myq", - "documentation": "https://www.home-assistant.io/components/myq", + "documentation": "https://www.home-assistant.io/integrations/myq", "requirements": [ "pymyq==1.2.1" ], diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index 536848d3aef645..8701424ea60e4a 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -1,7 +1,7 @@ { "domain": "mysensors", "name": "Mysensors", - "documentation": "https://www.home-assistant.io/components/mysensors", + "documentation": "https://www.home-assistant.io/integrations/mysensors", "requirements": [ "pymysensors==0.18.0" ], diff --git a/homeassistant/components/mystrom/manifest.json b/homeassistant/components/mystrom/manifest.json index 0e17f33f72ebd2..fe09461bc82f17 100644 --- a/homeassistant/components/mystrom/manifest.json +++ b/homeassistant/components/mystrom/manifest.json @@ -1,7 +1,7 @@ { "domain": "mystrom", "name": "Mystrom", - "documentation": "https://www.home-assistant.io/components/mystrom", + "documentation": "https://www.home-assistant.io/integrations/mystrom", "requirements": [ "python-mystrom==0.5.0" ], diff --git a/homeassistant/components/mythicbeastsdns/manifest.json b/homeassistant/components/mythicbeastsdns/manifest.json index 4e37544a99ad2c..b912a80f75d86a 100644 --- a/homeassistant/components/mythicbeastsdns/manifest.json +++ b/homeassistant/components/mythicbeastsdns/manifest.json @@ -1,7 +1,7 @@ { "domain": "mythicbeastsdns", "name": "Mythicbeastsdns", - "documentation": "https://www.home-assistant.io/components/mythicbeastsdns", + "documentation": "https://www.home-assistant.io/integrations/mythicbeastsdns", "requirements": [ "mbddns==0.1.2" ], diff --git a/homeassistant/components/n26/manifest.json b/homeassistant/components/n26/manifest.json index b49932887d504b..7f010ec6d39f52 100644 --- a/homeassistant/components/n26/manifest.json +++ b/homeassistant/components/n26/manifest.json @@ -1,7 +1,7 @@ { "domain": "n26", "name": "N26", - "documentation": "https://www.home-assistant.io/components/n26", + "documentation": "https://www.home-assistant.io/integrations/n26", "requirements": [ "n26==0.2.7" ], diff --git a/homeassistant/components/nad/manifest.json b/homeassistant/components/nad/manifest.json index c624acd73da5e4..7e01f818e351eb 100644 --- a/homeassistant/components/nad/manifest.json +++ b/homeassistant/components/nad/manifest.json @@ -1,7 +1,7 @@ { "domain": "nad", "name": "Nad", - "documentation": "https://www.home-assistant.io/components/nad", + "documentation": "https://www.home-assistant.io/integrations/nad", "requirements": [ "nad_receiver==0.0.11" ], diff --git a/homeassistant/components/namecheapdns/manifest.json b/homeassistant/components/namecheapdns/manifest.json index e75e2caa37ae22..aa1e2eb7422c0c 100644 --- a/homeassistant/components/namecheapdns/manifest.json +++ b/homeassistant/components/namecheapdns/manifest.json @@ -1,7 +1,7 @@ { "domain": "namecheapdns", "name": "Namecheapdns", - "documentation": "https://www.home-assistant.io/components/namecheapdns", + "documentation": "https://www.home-assistant.io/integrations/namecheapdns", "requirements": [ "defusedxml==0.6.0" ], diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index a59a6352af213d..3318ad3438fc35 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -1,7 +1,7 @@ { "domain": "nanoleaf", "name": "Nanoleaf", - "documentation": "https://www.home-assistant.io/components/nanoleaf", + "documentation": "https://www.home-assistant.io/integrations/nanoleaf", "requirements": [ "pynanoleaf==0.0.5" ], diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 71553fabc8e14f..8b0c5acc72362c 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -1,7 +1,7 @@ { "domain": "neato", "name": "Neato", - "documentation": "https://www.home-assistant.io/components/neato", + "documentation": "https://www.home-assistant.io/integrations/neato", "requirements": [ "pybotvac==0.0.15" ], diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index baa6551cc7c2fa..c1360ecbe3050b 100644 --- a/homeassistant/components/nederlandse_spoorwegen/manifest.json +++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json @@ -1,7 +1,7 @@ { "domain": "nederlandse_spoorwegen", "name": "Nederlandse spoorwegen", - "documentation": "https://www.home-assistant.io/components/nederlandse_spoorwegen", + "documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen", "requirements": [ "nsapi==2.7.4" ], diff --git a/homeassistant/components/nello/manifest.json b/homeassistant/components/nello/manifest.json index 0caafd7e27adf7..e747bf7739f06e 100644 --- a/homeassistant/components/nello/manifest.json +++ b/homeassistant/components/nello/manifest.json @@ -1,7 +1,7 @@ { "domain": "nello", "name": "Nello", - "documentation": "https://www.home-assistant.io/components/nello", + "documentation": "https://www.home-assistant.io/integrations/nello", "requirements": [ "pynello==2.0.2" ], diff --git a/homeassistant/components/ness_alarm/manifest.json b/homeassistant/components/ness_alarm/manifest.json index 93b19470ac434f..a1cae2df1d7319 100644 --- a/homeassistant/components/ness_alarm/manifest.json +++ b/homeassistant/components/ness_alarm/manifest.json @@ -1,7 +1,7 @@ { "domain": "ness_alarm", "name": "Ness alarm", - "documentation": "https://www.home-assistant.io/components/ness_alarm", + "documentation": "https://www.home-assistant.io/integrations/ness_alarm", "requirements": [ "nessclient==0.9.15" ], diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 8a6e8ec611afc9..b7d5f132045f0f 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,7 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/nest", + "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": [ "python-nest==4.1.0" ], diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 82f32c34407cab..83091368aff2ee 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -1,7 +1,7 @@ { "domain": "netatmo", "name": "Netatmo", - "documentation": "https://www.home-assistant.io/components/netatmo", + "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ "pyatmo==2.2.1" ], diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index 9c3b8ad33d23c9..da4d15c28aa838 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -1,7 +1,7 @@ { "domain": "netdata", "name": "Netdata", - "documentation": "https://www.home-assistant.io/components/netdata", + "documentation": "https://www.home-assistant.io/integrations/netdata", "requirements": [ "netdata==0.1.2" ], diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 3ee3b189939dc8..af709c755c8a9a 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -1,7 +1,7 @@ { "domain": "netgear", "name": "Netgear", - "documentation": "https://www.home-assistant.io/components/netgear", + "documentation": "https://www.home-assistant.io/integrations/netgear", "requirements": [ "pynetgear==0.6.1" ], diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 99ca3cb1ccfb82..7e085d063077aa 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -1,7 +1,7 @@ { "domain": "netgear_lte", "name": "Netgear lte", - "documentation": "https://www.home-assistant.io/components/netgear_lte", + "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "requirements": [ "eternalegypt==0.0.10" ], diff --git a/homeassistant/components/netio/manifest.json b/homeassistant/components/netio/manifest.json index e3675db04d7305..3755746aee1147 100644 --- a/homeassistant/components/netio/manifest.json +++ b/homeassistant/components/netio/manifest.json @@ -1,7 +1,7 @@ { "domain": "netio", "name": "Netio", - "documentation": "https://www.home-assistant.io/components/netio", + "documentation": "https://www.home-assistant.io/integrations/netio", "requirements": [ "pynetio==0.1.9.1" ], diff --git a/homeassistant/components/neurio_energy/manifest.json b/homeassistant/components/neurio_energy/manifest.json index 04420d5c4f2116..735baf58e5aeb6 100644 --- a/homeassistant/components/neurio_energy/manifest.json +++ b/homeassistant/components/neurio_energy/manifest.json @@ -1,7 +1,7 @@ { "domain": "neurio_energy", "name": "Neurio energy", - "documentation": "https://www.home-assistant.io/components/neurio_energy", + "documentation": "https://www.home-assistant.io/integrations/neurio_energy", "requirements": [ "neurio==0.3.1" ], diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 5c5a095c8f4cef..525947a8c74555 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -1,7 +1,7 @@ { "domain": "nextbus", "name": "NextBus", - "documentation": "https://www.home-assistant.io/components/nextbus", + "documentation": "https://www.home-assistant.io/integrations/nextbus", "dependencies": [], "codeowners": ["@vividboarder"], "requirements": ["py_nextbusnext==0.1.4"] diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index 8f3d88b58ee604..45eedf8a1540bd 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -1,7 +1,7 @@ { "domain": "nfandroidtv", "name": "Nfandroidtv", - "documentation": "https://www.home-assistant.io/components/nfandroidtv", + "documentation": "https://www.home-assistant.io/integrations/nfandroidtv", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/niko_home_control/manifest.json b/homeassistant/components/niko_home_control/manifest.json index 8cb58a7b74c81c..0e168601c4cf54 100644 --- a/homeassistant/components/niko_home_control/manifest.json +++ b/homeassistant/components/niko_home_control/manifest.json @@ -1,7 +1,7 @@ { "domain": "niko_home_control", "name": "Niko home control", - "documentation": "https://www.home-assistant.io/components/niko_home_control", + "documentation": "https://www.home-assistant.io/integrations/niko_home_control", "requirements": [ "niko-home-control==0.2.1" ], diff --git a/homeassistant/components/nilu/manifest.json b/homeassistant/components/nilu/manifest.json index ee7645653e6d0b..fe7a92bc2705bb 100644 --- a/homeassistant/components/nilu/manifest.json +++ b/homeassistant/components/nilu/manifest.json @@ -1,7 +1,7 @@ { "domain": "nilu", "name": "Nilu", - "documentation": "https://www.home-assistant.io/components/nilu", + "documentation": "https://www.home-assistant.io/integrations/nilu", "requirements": [ "niluclient==0.1.2" ], diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index 70aaa112414beb..a31812921470f4 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -1,7 +1,7 @@ { "domain": "nissan_leaf", "name": "Nissan leaf", - "documentation": "https://www.home-assistant.io/components/nissan_leaf", + "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", "requirements": [ "pycarwings2==2.9" ], diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 0380acba1aca58..b207bd07a423a3 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "nmap_tracker", "name": "Nmap tracker", - "documentation": "https://www.home-assistant.io/components/nmap_tracker", + "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", "requirements": [ "python-nmap==0.6.1", "getmac==0.8.1" diff --git a/homeassistant/components/nmbs/manifest.json b/homeassistant/components/nmbs/manifest.json index 1a2fa0556883f1..5fe5f743fd3a9c 100644 --- a/homeassistant/components/nmbs/manifest.json +++ b/homeassistant/components/nmbs/manifest.json @@ -1,7 +1,7 @@ { "domain": "nmbs", "name": "Nmbs", - "documentation": "https://www.home-assistant.io/components/nmbs", + "documentation": "https://www.home-assistant.io/integrations/nmbs", "requirements": [ "pyrail==0.0.3" ], diff --git a/homeassistant/components/no_ip/manifest.json b/homeassistant/components/no_ip/manifest.json index 125815995329e4..438b61136eb923 100644 --- a/homeassistant/components/no_ip/manifest.json +++ b/homeassistant/components/no_ip/manifest.json @@ -1,7 +1,7 @@ { "domain": "no_ip", "name": "No ip", - "documentation": "https://www.home-assistant.io/components/no_ip", + "documentation": "https://www.home-assistant.io/integrations/no_ip", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/noaa_tides/manifest.json b/homeassistant/components/noaa_tides/manifest.json index 9ffc0215fd15b7..069fc238f72101 100644 --- a/homeassistant/components/noaa_tides/manifest.json +++ b/homeassistant/components/noaa_tides/manifest.json @@ -1,7 +1,7 @@ { "domain": "noaa_tides", "name": "Noaa tides", - "documentation": "https://www.home-assistant.io/components/noaa_tides", + "documentation": "https://www.home-assistant.io/integrations/noaa_tides", "requirements": [ "py_noaa==0.3.0" ], diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 08c9932c36f5ce..e7ee2d2dcd5cbb 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -1,7 +1,7 @@ { "domain": "norway_air", "name": "Norway air", - "documentation": "https://www.home-assistant.io/components/norway_air", + "documentation": "https://www.home-assistant.io/integrations/norway_air", "requirements": [ "pyMetno==0.4.6" ], diff --git a/homeassistant/components/notify/manifest.json b/homeassistant/components/notify/manifest.json index bad39a1cb97ff5..6586496abbef27 100644 --- a/homeassistant/components/notify/manifest.json +++ b/homeassistant/components/notify/manifest.json @@ -1,7 +1,7 @@ { "domain": "notify", "name": "Notify", - "documentation": "https://www.home-assistant.io/components/notify", + "documentation": "https://www.home-assistant.io/integrations/notify", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index 827d406a1b5c79..2a400754b46526 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -2,7 +2,7 @@ "domain": "notion", "name": "Notion", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/notion", + "documentation": "https://www.home-assistant.io/integrations/notion", "requirements": [ "aionotion==1.1.0" ], diff --git a/homeassistant/components/nsw_fuel_station/manifest.json b/homeassistant/components/nsw_fuel_station/manifest.json index 6be24fb5a2cb0c..744497c22c47b2 100644 --- a/homeassistant/components/nsw_fuel_station/manifest.json +++ b/homeassistant/components/nsw_fuel_station/manifest.json @@ -1,7 +1,7 @@ { "domain": "nsw_fuel_station", "name": "Nsw fuel station", - "documentation": "https://www.home-assistant.io/components/nsw_fuel_station", + "documentation": "https://www.home-assistant.io/integrations/nsw_fuel_station", "requirements": [ "nsw-fuel-api-client==1.0.10" ], diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index 4542eb45c8222e..3d16f0a57e34cc 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -1,7 +1,7 @@ { "domain": "nsw_rural_fire_service_feed", "name": "Nsw rural fire service feed", - "documentation": "https://www.home-assistant.io/components/nsw_rural_fire_service_feed", + "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", "requirements": [ "geojson_client==0.4" ], diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index c9e69c44ec2e19..3d055f8f975436 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -1,7 +1,7 @@ { "domain": "nuheat", "name": "Nuheat", - "documentation": "https://www.home-assistant.io/components/nuheat", + "documentation": "https://www.home-assistant.io/integrations/nuheat", "requirements": [ "nuheat==0.3.0" ], diff --git a/homeassistant/components/nuimo_controller/manifest.json b/homeassistant/components/nuimo_controller/manifest.json index 9f18d2849f8ee3..a88faaa6f300b5 100644 --- a/homeassistant/components/nuimo_controller/manifest.json +++ b/homeassistant/components/nuimo_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "nuimo_controller", "name": "Nuimo controller", - "documentation": "https://www.home-assistant.io/components/nuimo_controller", + "documentation": "https://www.home-assistant.io/integrations/nuimo_controller", "requirements": [ "--only-binary=all nuimo==0.1.0" ], diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index e7f078a1a0594a..77043f37134f89 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -1,7 +1,7 @@ { "domain": "nuki", "name": "Nuki", - "documentation": "https://www.home-assistant.io/components/nuki", + "documentation": "https://www.home-assistant.io/integrations/nuki", "requirements": ["pynuki==1.3.3"], "dependencies": [], "codeowners": ["@pvizeli"] diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 920e56fba7cd47..21231c27873243 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -1,7 +1,7 @@ { "domain": "nut", "name": "Nut", - "documentation": "https://www.home-assistant.io/components/nut", + "documentation": "https://www.home-assistant.io/integrations/nut", "requirements": [ "pynut2==2.1.2" ], diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index bad90d9e827d77..b112a9ea4ea4c8 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -1,7 +1,7 @@ { "domain": "nws", "name": "National Weather Service", - "documentation": "https://www.home-assistant.io/components/nws", + "documentation": "https://www.home-assistant.io/integrations/nws", "dependencies": [], "codeowners": ["@MatthewFlamm"], "requirements": ["pynws==0.8.1"] diff --git a/homeassistant/components/nx584/manifest.json b/homeassistant/components/nx584/manifest.json index 67b5b0e2eeb96c..64f72986d077a2 100644 --- a/homeassistant/components/nx584/manifest.json +++ b/homeassistant/components/nx584/manifest.json @@ -1,7 +1,7 @@ { "domain": "nx584", "name": "Nx584", - "documentation": "https://www.home-assistant.io/components/nx584", + "documentation": "https://www.home-assistant.io/integrations/nx584", "requirements": [ "pynx584==0.4" ], diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json index 17b11d6aef9fdc..1dc16fdba407d2 100644 --- a/homeassistant/components/nzbget/manifest.json +++ b/homeassistant/components/nzbget/manifest.json @@ -1,7 +1,7 @@ { "domain": "nzbget", "name": "Nzbget", - "documentation": "https://www.home-assistant.io/components/nzbget", + "documentation": "https://www.home-assistant.io/integrations/nzbget", "requirements": ["pynzbgetapi==0.2.0"], "dependencies": [], "codeowners": ["@chriscla"] diff --git a/homeassistant/components/oasa_telematics/manifest.json b/homeassistant/components/oasa_telematics/manifest.json index 15bf40e63c865c..e6cc998352df66 100644 --- a/homeassistant/components/oasa_telematics/manifest.json +++ b/homeassistant/components/oasa_telematics/manifest.json @@ -1,7 +1,7 @@ { "domain": "oasa_telematics", "name": "OASA Telematics", - "documentation": "https://www.home-assistant.io/components/oasa_telematics/", + "documentation": "https://www.home-assistant.io/integrations/oasa_telematics/", "requirements": [ "oasatelematics==0.3" ], diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index 68045ff0584684..09087663b47037 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -1,7 +1,7 @@ { "domain": "obihai", "name": "Obihai", - "documentation": "https://www.home-assistant.io/components/obihai", + "documentation": "https://www.home-assistant.io/integrations/obihai", "requirements": [ "pyobihai==1.2.0" ], diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index c34e1458e4bdb5..77f6a22e69f5a6 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -1,7 +1,7 @@ { "domain": "octoprint", "name": "Octoprint", - "documentation": "https://www.home-assistant.io/components/octoprint", + "documentation": "https://www.home-assistant.io/integrations/octoprint", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/oem/manifest.json b/homeassistant/components/oem/manifest.json index d23b07b2756633..1634676a60beeb 100644 --- a/homeassistant/components/oem/manifest.json +++ b/homeassistant/components/oem/manifest.json @@ -1,7 +1,7 @@ { "domain": "oem", "name": "Oem", - "documentation": "https://www.home-assistant.io/components/oem", + "documentation": "https://www.home-assistant.io/integrations/oem", "requirements": [ "oemthermostat==1.1" ], diff --git a/homeassistant/components/ohmconnect/manifest.json b/homeassistant/components/ohmconnect/manifest.json index 33c93bc8ac139a..c958d23e725d6a 100644 --- a/homeassistant/components/ohmconnect/manifest.json +++ b/homeassistant/components/ohmconnect/manifest.json @@ -1,7 +1,7 @@ { "domain": "ohmconnect", "name": "Ohmconnect", - "documentation": "https://www.home-assistant.io/components/ohmconnect", + "documentation": "https://www.home-assistant.io/integrations/ohmconnect", "requirements": [ "defusedxml==0.6.0" ], diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json index 066f3270ccdd5b..fb6daf00f6646b 100644 --- a/homeassistant/components/ombi/manifest.json +++ b/homeassistant/components/ombi/manifest.json @@ -1,7 +1,7 @@ { "domain": "ombi", "name": "Ombi", - "documentation": "https://www.home-assistant.io/components/ombi/", + "documentation": "https://www.home-assistant.io/integrations/ombi/", "dependencies": [], "codeowners": ["@larssont"], "requirements": ["pyombi==0.1.5"] diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index ffb01bd56021d5..2febfc481e01f9 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -1,7 +1,7 @@ { "domain": "onboarding", "name": "Onboarding", - "documentation": "https://www.home-assistant.io/components/onboarding", + "documentation": "https://www.home-assistant.io/integrations/onboarding", "requirements": [], "dependencies": [ "auth", diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index 00075d4485f4e4..2d8c6c7107107e 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -1,7 +1,7 @@ { "domain": "onewire", "name": "Onewire", - "documentation": "https://www.home-assistant.io/components/onewire", + "documentation": "https://www.home-assistant.io/integrations/onewire", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index 7fd27dd7edf8b1..5bb116dece8efa 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -1,7 +1,7 @@ { "domain": "onkyo", "name": "Onkyo", - "documentation": "https://www.home-assistant.io/components/onkyo", + "documentation": "https://www.home-assistant.io/integrations/onkyo", "requirements": [ "onkyo-eiscp==1.2.4" ], diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index d86ec38ccb7178..f6c23712188ca9 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -1,7 +1,7 @@ { "domain": "onvif", "name": "Onvif", - "documentation": "https://www.home-assistant.io/components/onvif", + "documentation": "https://www.home-assistant.io/integrations/onvif", "requirements": [ "onvif-zeep-async==0.2.0" ], diff --git a/homeassistant/components/openalpr_cloud/manifest.json b/homeassistant/components/openalpr_cloud/manifest.json index f0421295836f03..56128f0e8866e0 100644 --- a/homeassistant/components/openalpr_cloud/manifest.json +++ b/homeassistant/components/openalpr_cloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "openalpr_cloud", "name": "Openalpr cloud", - "documentation": "https://www.home-assistant.io/components/openalpr_cloud", + "documentation": "https://www.home-assistant.io/integrations/openalpr_cloud", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json index 3c92e840f43406..65fa6b1bec6d6a 100644 --- a/homeassistant/components/openalpr_local/manifest.json +++ b/homeassistant/components/openalpr_local/manifest.json @@ -1,7 +1,7 @@ { "domain": "openalpr_local", "name": "Openalpr local", - "documentation": "https://www.home-assistant.io/components/openalpr_local", + "documentation": "https://www.home-assistant.io/integrations/openalpr_local", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index e8ebeb102e67b1..40421674a4b203 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -1,7 +1,7 @@ { "domain": "opencv", "name": "Opencv", - "documentation": "https://www.home-assistant.io/components/opencv", + "documentation": "https://www.home-assistant.io/integrations/opencv", "requirements": [ "numpy==1.17.1", "opencv-python-headless==4.1.1.26" diff --git a/homeassistant/components/openevse/manifest.json b/homeassistant/components/openevse/manifest.json index f37c769d20e5be..a56f0caddabedd 100644 --- a/homeassistant/components/openevse/manifest.json +++ b/homeassistant/components/openevse/manifest.json @@ -1,7 +1,7 @@ { "domain": "openevse", "name": "Openevse", - "documentation": "https://www.home-assistant.io/components/openevse", + "documentation": "https://www.home-assistant.io/integrations/openevse", "requirements": [ "openevsewifi==0.4" ], diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index ffb86d4a5e2d6c..fae9e616e5e737 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -1,7 +1,7 @@ { "domain": "openexchangerates", "name": "Openexchangerates", - "documentation": "https://www.home-assistant.io/components/openexchangerates", + "documentation": "https://www.home-assistant.io/integrations/openexchangerates", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index 95f944b7087a75..4dcb53e98ce369 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -1,7 +1,7 @@ { "domain": "opengarage", "name": "Opengarage", - "documentation": "https://www.home-assistant.io/components/opengarage", + "documentation": "https://www.home-assistant.io/integrations/opengarage", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/openhardwaremonitor/manifest.json b/homeassistant/components/openhardwaremonitor/manifest.json index d9281f08eda102..4a17f9335150ec 100644 --- a/homeassistant/components/openhardwaremonitor/manifest.json +++ b/homeassistant/components/openhardwaremonitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "openhardwaremonitor", "name": "Openhardwaremonitor", - "documentation": "https://www.home-assistant.io/components/openhardwaremonitor", + "documentation": "https://www.home-assistant.io/integrations/openhardwaremonitor", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 276346ae79bff0..2ec58b86125fb8 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -1,7 +1,7 @@ { "domain": "openhome", "name": "Openhome", - "documentation": "https://www.home-assistant.io/components/openhome", + "documentation": "https://www.home-assistant.io/integrations/openhome", "requirements": [ "openhomedevice==0.4.2" ], diff --git a/homeassistant/components/opensensemap/manifest.json b/homeassistant/components/opensensemap/manifest.json index ab03f1cf7c6824..632ab82918e036 100644 --- a/homeassistant/components/opensensemap/manifest.json +++ b/homeassistant/components/opensensemap/manifest.json @@ -1,7 +1,7 @@ { "domain": "opensensemap", "name": "Opensensemap", - "documentation": "https://www.home-assistant.io/components/opensensemap", + "documentation": "https://www.home-assistant.io/integrations/opensensemap", "requirements": [ "opensensemap-api==0.1.5" ], diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index dd58cdd416816b..a98e828a52ef80 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -1,7 +1,7 @@ { "domain": "opensky", "name": "Opensky", - "documentation": "https://www.home-assistant.io/components/opensky", + "documentation": "https://www.home-assistant.io/integrations/opensky", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index c6097a01cc421c..9c7f165c6dfa19 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -1,7 +1,7 @@ { "domain": "opentherm_gw", "name": "Opentherm Gateway", - "documentation": "https://www.home-assistant.io/components/opentherm_gw", + "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "requirements": [ "pyotgw==0.4b4" ], diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index 0cfb02e81d64eb..69342df235f38f 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -2,7 +2,7 @@ "domain": "openuv", "name": "Openuv", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/openuv", + "documentation": "https://www.home-assistant.io/integrations/openuv", "requirements": [ "pyopenuv==1.0.9" ], diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index d24b23f64bb470..c28d27c1b8b5d1 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -1,7 +1,7 @@ { "domain": "openweathermap", "name": "Openweathermap", - "documentation": "https://www.home-assistant.io/components/openweathermap", + "documentation": "https://www.home-assistant.io/integrations/openweathermap", "requirements": [ "pyowm==2.10.0" ], diff --git a/homeassistant/components/opple/manifest.json b/homeassistant/components/opple/manifest.json index c10be48f3fa3cc..cee6447abc1afc 100644 --- a/homeassistant/components/opple/manifest.json +++ b/homeassistant/components/opple/manifest.json @@ -1,7 +1,7 @@ { "domain": "opple", "name": "Opple", - "documentation": "https://www.home-assistant.io/components/opple", + "documentation": "https://www.home-assistant.io/integrations/opple", "requirements": [ "pyoppleio==1.0.5" ], diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json index 65fd0f7de50836..51bca8fbbbe85c 100644 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ b/homeassistant/components/orangepi_gpio/manifest.json @@ -1,7 +1,7 @@ { "domain": "orangepi_gpio", "name": "Orangepi GPIO", - "documentation": "https://www.home-assistant.io/components/orangepi_gpio", + "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", "requirements": [ "OPi.GPIO==0.3.6" ], diff --git a/homeassistant/components/orvibo/manifest.json b/homeassistant/components/orvibo/manifest.json index 73f4eaed7dae11..9dee62697c34ee 100644 --- a/homeassistant/components/orvibo/manifest.json +++ b/homeassistant/components/orvibo/manifest.json @@ -1,7 +1,7 @@ { "domain": "orvibo", "name": "Orvibo", - "documentation": "https://www.home-assistant.io/components/orvibo", + "documentation": "https://www.home-assistant.io/integrations/orvibo", "requirements": [ "orvibo==1.1.1" ], diff --git a/homeassistant/components/osramlightify/manifest.json b/homeassistant/components/osramlightify/manifest.json index 0b158b967423b9..8c6c9f30b1060b 100644 --- a/homeassistant/components/osramlightify/manifest.json +++ b/homeassistant/components/osramlightify/manifest.json @@ -1,7 +1,7 @@ { "domain": "osramlightify", "name": "Osramlightify", - "documentation": "https://www.home-assistant.io/components/osramlightify", + "documentation": "https://www.home-assistant.io/integrations/osramlightify", "requirements": [ "lightify==1.0.7.2" ], diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index 112fca24194a8e..25ece71c11b56b 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -1,7 +1,7 @@ { "domain": "otp", "name": "Otp", - "documentation": "https://www.home-assistant.io/components/otp", + "documentation": "https://www.home-assistant.io/integrations/otp", "requirements": [ "pyotp==2.3.0" ], diff --git a/homeassistant/components/owlet/manifest.json b/homeassistant/components/owlet/manifest.json index b89947343c94f4..2ba00671b485fe 100644 --- a/homeassistant/components/owlet/manifest.json +++ b/homeassistant/components/owlet/manifest.json @@ -1,7 +1,7 @@ { "domain": "owlet", "name": "Owlet", - "documentation": "https://www.home-assistant.io/components/owlet", + "documentation": "https://www.home-assistant.io/integrations/owlet", "requirements": [ "pyowlet==1.0.3" ], diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index bc4fe97bc7f138..529d7990a8644e 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -2,7 +2,7 @@ "domain": "owntracks", "name": "Owntracks", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/owntracks", + "documentation": "https://www.home-assistant.io/integrations/owntracks", "requirements": [ "PyNaCl==1.3.0" ], diff --git a/homeassistant/components/panasonic_bluray/manifest.json b/homeassistant/components/panasonic_bluray/manifest.json index fe2387744ab216..7f386464dc99a3 100644 --- a/homeassistant/components/panasonic_bluray/manifest.json +++ b/homeassistant/components/panasonic_bluray/manifest.json @@ -1,7 +1,7 @@ { "domain": "panasonic_bluray", "name": "Panasonic bluray", - "documentation": "https://www.home-assistant.io/components/panasonic_bluray", + "documentation": "https://www.home-assistant.io/integrations/panasonic_bluray", "requirements": [ "panacotta==0.1" ], diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index 432e729ef20a0c..e7e06479eec72a 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -1,7 +1,7 @@ { "domain": "panasonic_viera", "name": "Panasonic viera", - "documentation": "https://www.home-assistant.io/components/panasonic_viera", + "documentation": "https://www.home-assistant.io/integrations/panasonic_viera", "requirements": [ "panasonic_viera==0.3.2", "wakeonlan==1.1.6" diff --git a/homeassistant/components/pandora/manifest.json b/homeassistant/components/pandora/manifest.json index 68e8337a33db05..a15267b7d85ef6 100644 --- a/homeassistant/components/pandora/manifest.json +++ b/homeassistant/components/pandora/manifest.json @@ -1,7 +1,7 @@ { "domain": "pandora", "name": "Pandora", - "documentation": "https://www.home-assistant.io/components/pandora", + "documentation": "https://www.home-assistant.io/integrations/pandora", "requirements": [ "pexpect==4.6.0" ], diff --git a/homeassistant/components/panel_custom/manifest.json b/homeassistant/components/panel_custom/manifest.json index 06c9338742cb0a..5d0cc555705774 100644 --- a/homeassistant/components/panel_custom/manifest.json +++ b/homeassistant/components/panel_custom/manifest.json @@ -1,7 +1,7 @@ { "domain": "panel_custom", "name": "Panel custom", - "documentation": "https://www.home-assistant.io/components/panel_custom", + "documentation": "https://www.home-assistant.io/integrations/panel_custom", "requirements": [], "dependencies": [ "frontend" diff --git a/homeassistant/components/panel_iframe/manifest.json b/homeassistant/components/panel_iframe/manifest.json index e66f94bdcc20f3..6d7c66f6097f9e 100644 --- a/homeassistant/components/panel_iframe/manifest.json +++ b/homeassistant/components/panel_iframe/manifest.json @@ -1,7 +1,7 @@ { "domain": "panel_iframe", "name": "Panel iframe", - "documentation": "https://www.home-assistant.io/components/panel_iframe", + "documentation": "https://www.home-assistant.io/integrations/panel_iframe", "requirements": [], "dependencies": [ "frontend" diff --git a/homeassistant/components/pencom/manifest.json b/homeassistant/components/pencom/manifest.json index 186e071d25b55d..d31b9a811cfc13 100644 --- a/homeassistant/components/pencom/manifest.json +++ b/homeassistant/components/pencom/manifest.json @@ -1,7 +1,7 @@ { "domain": "pencom", "name": "Pencom", - "documentation": "https://www.home-assistant.io/components/pencom", + "documentation": "https://www.home-assistant.io/integrations/pencom", "requirements": [ "pencompy==0.0.3" ], diff --git a/homeassistant/components/persistent_notification/manifest.json b/homeassistant/components/persistent_notification/manifest.json index 8bc343e1f0876a..9ee9692c65530c 100644 --- a/homeassistant/components/persistent_notification/manifest.json +++ b/homeassistant/components/persistent_notification/manifest.json @@ -1,7 +1,7 @@ { "domain": "persistent_notification", "name": "Persistent notification", - "documentation": "https://www.home-assistant.io/components/persistent_notification", + "documentation": "https://www.home-assistant.io/integrations/persistent_notification", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index d2cba929259248..cf50b8029c2e91 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -1,7 +1,7 @@ { "domain": "person", "name": "Person", - "documentation": "https://www.home-assistant.io/components/person", + "documentation": "https://www.home-assistant.io/integrations/person", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 0b1579a139df40..4845aa16a37576 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -1,7 +1,7 @@ { "domain": "philips_js", "name": "Philips js", - "documentation": "https://www.home-assistant.io/components/philips_js", + "documentation": "https://www.home-assistant.io/integrations/philips_js", "requirements": [ "ha-philipsjs==0.0.8" ], diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index 7fe8bba6873913..089e1e60a11d81 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -1,7 +1,7 @@ { "domain": "pi_hole", "name": "Pi hole", - "documentation": "https://www.home-assistant.io/components/pi_hole", + "documentation": "https://www.home-assistant.io/integrations/pi_hole", "requirements": [ "hole==0.5.0" ], diff --git a/homeassistant/components/picotts/manifest.json b/homeassistant/components/picotts/manifest.json index bfe7f449ca073b..5150ddd0404dae 100644 --- a/homeassistant/components/picotts/manifest.json +++ b/homeassistant/components/picotts/manifest.json @@ -1,7 +1,7 @@ { "domain": "picotts", "name": "Picotts", - "documentation": "https://www.home-assistant.io/components/picotts", + "documentation": "https://www.home-assistant.io/integrations/picotts", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/piglow/manifest.json b/homeassistant/components/piglow/manifest.json index 67b1033c51ea42..012ce4f014ee7e 100644 --- a/homeassistant/components/piglow/manifest.json +++ b/homeassistant/components/piglow/manifest.json @@ -1,7 +1,7 @@ { "domain": "piglow", "name": "Piglow", - "documentation": "https://www.home-assistant.io/components/piglow", + "documentation": "https://www.home-assistant.io/integrations/piglow", "requirements": [ "piglow==1.2.4" ], diff --git a/homeassistant/components/pilight/manifest.json b/homeassistant/components/pilight/manifest.json index dfe4952e1a17fb..7613f9e2a3477b 100644 --- a/homeassistant/components/pilight/manifest.json +++ b/homeassistant/components/pilight/manifest.json @@ -1,7 +1,7 @@ { "domain": "pilight", "name": "Pilight", - "documentation": "https://www.home-assistant.io/components/pilight", + "documentation": "https://www.home-assistant.io/integrations/pilight", "requirements": [ "pilight==0.1.1" ], diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index d98adef87a7bd4..19e84365fac19b 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -1,7 +1,7 @@ { "domain": "ping", "name": "Ping", - "documentation": "https://www.home-assistant.io/components/ping", + "documentation": "https://www.home-assistant.io/integrations/ping", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/pioneer/manifest.json b/homeassistant/components/pioneer/manifest.json index b06874149ed17e..3aa046f234d6b2 100644 --- a/homeassistant/components/pioneer/manifest.json +++ b/homeassistant/components/pioneer/manifest.json @@ -1,7 +1,7 @@ { "domain": "pioneer", "name": "Pioneer", - "documentation": "https://www.home-assistant.io/components/pioneer", + "documentation": "https://www.home-assistant.io/integrations/pioneer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/pjlink/manifest.json b/homeassistant/components/pjlink/manifest.json index 6901847bd8d0bd..3c2b67e3425693 100644 --- a/homeassistant/components/pjlink/manifest.json +++ b/homeassistant/components/pjlink/manifest.json @@ -1,7 +1,7 @@ { "domain": "pjlink", "name": "Pjlink", - "documentation": "https://www.home-assistant.io/components/pjlink", + "documentation": "https://www.home-assistant.io/integrations/pjlink", "requirements": [ "pypjlink2==1.2.0" ], diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json index cd6111ba9da789..658173d3db2eaf 100644 --- a/homeassistant/components/plaato/manifest.json +++ b/homeassistant/components/plaato/manifest.json @@ -2,7 +2,7 @@ "domain": "plaato", "name": "Plaato Airlock", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/plaato", + "documentation": "https://www.home-assistant.io/integrations/plaato", "dependencies": ["webhook"], "codeowners": ["@JohNan"], "requirements": [] diff --git a/homeassistant/components/plant/manifest.json b/homeassistant/components/plant/manifest.json index cbde894173b7cf..721a57e782248a 100644 --- a/homeassistant/components/plant/manifest.json +++ b/homeassistant/components/plant/manifest.json @@ -1,7 +1,7 @@ { "domain": "plant", "name": "Plant", - "documentation": "https://www.home-assistant.io/components/plant", + "documentation": "https://www.home-assistant.io/integrations/plant", "requirements": [], "dependencies": [ "group", diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 8e068c909c5d5e..d4f2ae0517a296 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -2,7 +2,7 @@ "domain": "plex", "name": "Plex", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/plex", + "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ "plexapi==3.0.6", "plexauth==0.0.4" diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index c399232f3151b1..1069c0bcdf02bd 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -1,7 +1,7 @@ { "domain": "plugwise", "name": "Plugwise", - "documentation": "https://www.home-assistant.io/components/plugwise", + "documentation": "https://www.home-assistant.io/integrations/plugwise", "dependencies": [], "codeowners": ["@laetificat","@CoMPaTech"], "requirements": ["haanna==0.10.1"] diff --git a/homeassistant/components/plum_lightpad/manifest.json b/homeassistant/components/plum_lightpad/manifest.json index 389eca09c4238d..8c2da09026977b 100644 --- a/homeassistant/components/plum_lightpad/manifest.json +++ b/homeassistant/components/plum_lightpad/manifest.json @@ -1,7 +1,7 @@ { "domain": "plum_lightpad", "name": "Plum lightpad", - "documentation": "https://www.home-assistant.io/components/plum_lightpad", + "documentation": "https://www.home-assistant.io/integrations/plum_lightpad", "requirements": [ "plumlightpad==0.0.11" ], diff --git a/homeassistant/components/pocketcasts/manifest.json b/homeassistant/components/pocketcasts/manifest.json index 11c202363246a7..f72984f9fc0e38 100644 --- a/homeassistant/components/pocketcasts/manifest.json +++ b/homeassistant/components/pocketcasts/manifest.json @@ -1,7 +1,7 @@ { "domain": "pocketcasts", "name": "Pocketcasts", - "documentation": "https://www.home-assistant.io/components/pocketcasts", + "documentation": "https://www.home-assistant.io/integrations/pocketcasts", "requirements": [ "pocketcasts==0.1" ], diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index fcc9265ce9b4fe..0f2faef86dfaed 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -2,7 +2,7 @@ "domain": "point", "name": "Point", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/point", + "documentation": "https://www.home-assistant.io/integrations/point", "requirements": [ "pypoint==1.1.1" ], diff --git a/homeassistant/components/postnl/manifest.json b/homeassistant/components/postnl/manifest.json index 9746cb168aa137..d07f9746ee8f28 100644 --- a/homeassistant/components/postnl/manifest.json +++ b/homeassistant/components/postnl/manifest.json @@ -1,7 +1,7 @@ { "domain": "postnl", "name": "Postnl", - "documentation": "https://www.home-assistant.io/components/postnl", + "documentation": "https://www.home-assistant.io/integrations/postnl", "requirements": [ "postnl_api==1.0.2" ], diff --git a/homeassistant/components/prezzibenzina/manifest.json b/homeassistant/components/prezzibenzina/manifest.json index 2427ebbfdb0508..99a8a1e27019f8 100644 --- a/homeassistant/components/prezzibenzina/manifest.json +++ b/homeassistant/components/prezzibenzina/manifest.json @@ -1,7 +1,7 @@ { "domain": "prezzibenzina", "name": "Prezzibenzina", - "documentation": "https://www.home-assistant.io/components/prezzibenzina", + "documentation": "https://www.home-assistant.io/integrations/prezzibenzina", "requirements": [ "prezzibenzina-py==1.1.4" ], diff --git a/homeassistant/components/proliphix/manifest.json b/homeassistant/components/proliphix/manifest.json index 3aa356823c1823..a035657a090248 100644 --- a/homeassistant/components/proliphix/manifest.json +++ b/homeassistant/components/proliphix/manifest.json @@ -1,7 +1,7 @@ { "domain": "proliphix", "name": "Proliphix", - "documentation": "https://www.home-assistant.io/components/proliphix", + "documentation": "https://www.home-assistant.io/integrations/proliphix", "requirements": [ "proliphix==0.4.1" ], diff --git a/homeassistant/components/prometheus/manifest.json b/homeassistant/components/prometheus/manifest.json index cab1228aa5682e..309284e5f62b16 100644 --- a/homeassistant/components/prometheus/manifest.json +++ b/homeassistant/components/prometheus/manifest.json @@ -1,7 +1,7 @@ { "domain": "prometheus", "name": "Prometheus", - "documentation": "https://www.home-assistant.io/components/prometheus", + "documentation": "https://www.home-assistant.io/integrations/prometheus", "requirements": [ "prometheus_client==0.7.1" ], diff --git a/homeassistant/components/prowl/manifest.json b/homeassistant/components/prowl/manifest.json index a8b4893c995a29..73aa818a2addc5 100644 --- a/homeassistant/components/prowl/manifest.json +++ b/homeassistant/components/prowl/manifest.json @@ -1,7 +1,7 @@ { "domain": "prowl", "name": "Prowl", - "documentation": "https://www.home-assistant.io/components/prowl", + "documentation": "https://www.home-assistant.io/integrations/prowl", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/proximity/manifest.json b/homeassistant/components/proximity/manifest.json index 335bea82fc91d8..708a3fb2616ed9 100644 --- a/homeassistant/components/proximity/manifest.json +++ b/homeassistant/components/proximity/manifest.json @@ -1,7 +1,7 @@ { "domain": "proximity", "name": "Proximity", - "documentation": "https://www.home-assistant.io/components/proximity", + "documentation": "https://www.home-assistant.io/integrations/proximity", "requirements": [], "dependencies": [ "device_tracker", diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 54d5ebe5f14bdc..c67fd4afc090c5 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -1,7 +1,7 @@ { "domain": "proxy", "name": "Proxy", - "documentation": "https://www.home-assistant.io/components/proxy", + "documentation": "https://www.home-assistant.io/integrations/proxy", "requirements": [ "pillow==6.1.0" ], diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 797a5432f9722b..98a14d877e8f72 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -2,7 +2,7 @@ "domain": "ps4", "name": "Ps4", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ps4", + "documentation": "https://www.home-assistant.io/integrations/ps4", "requirements": [ "pyps4-homeassistant==0.8.7" ], diff --git a/homeassistant/components/ptvsd/manifest.json b/homeassistant/components/ptvsd/manifest.json index 8bd46c3dc32f1b..2f8398531c729b 100644 --- a/homeassistant/components/ptvsd/manifest.json +++ b/homeassistant/components/ptvsd/manifest.json @@ -1,7 +1,7 @@ { "domain": "ptvsd", "name": "ptvsd", - "documentation": "https://www.home-assistant.io/components/ptvsd", + "documentation": "https://www.home-assistant.io/integrations/ptvsd", "requirements": [ "ptvsd==4.2.8" ], diff --git a/homeassistant/components/pulseaudio_loopback/manifest.json b/homeassistant/components/pulseaudio_loopback/manifest.json index 58a2871e027939..19247e1a7d5b80 100644 --- a/homeassistant/components/pulseaudio_loopback/manifest.json +++ b/homeassistant/components/pulseaudio_loopback/manifest.json @@ -1,7 +1,7 @@ { "domain": "pulseaudio_loopback", "name": "Pulseaudio loopback", - "documentation": "https://www.home-assistant.io/components/pulseaudio_loopback", + "documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/push/manifest.json b/homeassistant/components/push/manifest.json index 278638caff8841..161ea29b3b3393 100644 --- a/homeassistant/components/push/manifest.json +++ b/homeassistant/components/push/manifest.json @@ -1,7 +1,7 @@ { "domain": "push", "name": "Push", - "documentation": "https://www.home-assistant.io/components/push", + "documentation": "https://www.home-assistant.io/integrations/push", "requirements": [], "dependencies": ["webhook"], "codeowners": [ diff --git a/homeassistant/components/pushbullet/manifest.json b/homeassistant/components/pushbullet/manifest.json index 51e77959d7ad9c..f649e5467a4580 100644 --- a/homeassistant/components/pushbullet/manifest.json +++ b/homeassistant/components/pushbullet/manifest.json @@ -1,7 +1,7 @@ { "domain": "pushbullet", "name": "Pushbullet", - "documentation": "https://www.home-assistant.io/components/pushbullet", + "documentation": "https://www.home-assistant.io/integrations/pushbullet", "requirements": [ "pushbullet.py==0.11.0" ], diff --git a/homeassistant/components/pushetta/manifest.json b/homeassistant/components/pushetta/manifest.json index b42180c7268035..3000f9fd17bc25 100644 --- a/homeassistant/components/pushetta/manifest.json +++ b/homeassistant/components/pushetta/manifest.json @@ -1,7 +1,7 @@ { "domain": "pushetta", "name": "Pushetta", - "documentation": "https://www.home-assistant.io/components/pushetta", + "documentation": "https://www.home-assistant.io/integrations/pushetta", "requirements": [ "pushetta==1.0.15" ], diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index 1cdbb4ff48b012..01c3fc270fbe5c 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -1,7 +1,7 @@ { "domain": "pushover", "name": "Pushover", - "documentation": "https://www.home-assistant.io/components/pushover", + "documentation": "https://www.home-assistant.io/integrations/pushover", "requirements": [ "python-pushover==0.4" ], diff --git a/homeassistant/components/pushsafer/manifest.json b/homeassistant/components/pushsafer/manifest.json index 300d0ead4a5c3a..18592124c24d4b 100644 --- a/homeassistant/components/pushsafer/manifest.json +++ b/homeassistant/components/pushsafer/manifest.json @@ -1,7 +1,7 @@ { "domain": "pushsafer", "name": "Pushsafer", - "documentation": "https://www.home-assistant.io/components/pushsafer", + "documentation": "https://www.home-assistant.io/integrations/pushsafer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index b61c7100828f09..7e6879de9a1437 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -1,7 +1,7 @@ { "domain": "pvoutput", "name": "Pvoutput", - "documentation": "https://www.home-assistant.io/components/pvoutput", + "documentation": "https://www.home-assistant.io/integrations/pvoutput", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 437bd3bc4d2ce1..4cbcd8321b597b 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -1,7 +1,7 @@ { "domain": "pyload", "name": "Pyload", - "documentation": "https://www.home-assistant.io/components/pyload", + "documentation": "https://www.home-assistant.io/integrations/pyload", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 83d70830b11a43..2e5c12082100f8 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -1,7 +1,7 @@ { "domain": "python_script", "name": "Python script", - "documentation": "https://www.home-assistant.io/components/python_script", + "documentation": "https://www.home-assistant.io/integrations/python_script", "requirements": [ "restrictedpython==5.0" ], diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index 5fb850739d8d0d..c41d4ba46d362f 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -1,7 +1,7 @@ { "domain": "qbittorrent", "name": "Qbittorrent", - "documentation": "https://www.home-assistant.io/components/qbittorrent", + "documentation": "https://www.home-assistant.io/integrations/qbittorrent", "requirements": [ "python-qbittorrent==0.3.1" ], diff --git a/homeassistant/components/qld_bushfire/manifest.json b/homeassistant/components/qld_bushfire/manifest.json index 47a4a4b5f85ce1..c113e6034cd904 100644 --- a/homeassistant/components/qld_bushfire/manifest.json +++ b/homeassistant/components/qld_bushfire/manifest.json @@ -1,7 +1,7 @@ { "domain": "qld_bushfire", "name": "Queensland Bushfire Alert", - "documentation": "https://www.home-assistant.io/components/qld_bushfire", + "documentation": "https://www.home-assistant.io/integrations/qld_bushfire", "requirements": [ "georss_qld_bushfire_alert_client==0.3" ], diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json index f02d416c7e6be9..a34c49645ceae8 100644 --- a/homeassistant/components/qnap/manifest.json +++ b/homeassistant/components/qnap/manifest.json @@ -1,7 +1,7 @@ { "domain": "qnap", "name": "Qnap", - "documentation": "https://www.home-assistant.io/components/qnap", + "documentation": "https://www.home-assistant.io/integrations/qnap", "requirements": [ "qnapstats==0.2.7" ], diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index eb8da25bace095..87e16f629876e8 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -1,7 +1,7 @@ { "domain": "qrcode", "name": "Qrcode", - "documentation": "https://www.home-assistant.io/components/qrcode", + "documentation": "https://www.home-assistant.io/integrations/qrcode", "requirements": [ "pillow==6.1.0", "pyzbar==0.1.7" diff --git a/homeassistant/components/quantum_gateway/manifest.json b/homeassistant/components/quantum_gateway/manifest.json index 9c062482a4c2f2..da2fc20510fee4 100644 --- a/homeassistant/components/quantum_gateway/manifest.json +++ b/homeassistant/components/quantum_gateway/manifest.json @@ -1,7 +1,7 @@ { "domain": "quantum_gateway", "name": "Quantum gateway", - "documentation": "https://www.home-assistant.io/components/quantum_gateway", + "documentation": "https://www.home-assistant.io/integrations/quantum_gateway", "requirements": [ "quantum-gateway==0.0.5" ], diff --git a/homeassistant/components/qwikswitch/manifest.json b/homeassistant/components/qwikswitch/manifest.json index 4907cb462b6f1a..6d2944282ba550 100644 --- a/homeassistant/components/qwikswitch/manifest.json +++ b/homeassistant/components/qwikswitch/manifest.json @@ -1,7 +1,7 @@ { "domain": "qwikswitch", "name": "Qwikswitch", - "documentation": "https://www.home-assistant.io/components/qwikswitch", + "documentation": "https://www.home-assistant.io/integrations/qwikswitch", "requirements": [ "pyqwikswitch==0.93" ], diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index 30bde9a297d396..79e3677d65e821 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -1,7 +1,7 @@ { "domain": "rachio", "name": "Rachio", - "documentation": "https://www.home-assistant.io/components/rachio", + "documentation": "https://www.home-assistant.io/integrations/rachio", "requirements": [ "rachiopy==0.1.3" ], diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index f12fcf4220cfe9..2683525e7b44fd 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -1,7 +1,7 @@ { "domain": "radarr", "name": "Radarr", - "documentation": "https://www.home-assistant.io/components/radarr", + "documentation": "https://www.home-assistant.io/integrations/radarr", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index 002fdb632739c0..c3f8079cccdf43 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -1,7 +1,7 @@ { "domain": "radiotherm", "name": "Radiotherm", - "documentation": "https://www.home-assistant.io/components/radiotherm", + "documentation": "https://www.home-assistant.io/integrations/radiotherm", "requirements": [ "radiotherm==2.0.0" ], diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index b911aaa57e19b5..bb421c725ca69f 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -1,7 +1,7 @@ { "domain": "rainbird", "name": "Rainbird", - "documentation": "https://www.home-assistant.io/components/rainbird", + "documentation": "https://www.home-assistant.io/integrations/rainbird", "requirements": [ "pyrainbird==0.4.1" ], diff --git a/homeassistant/components/raincloud/manifest.json b/homeassistant/components/raincloud/manifest.json index 4d07f2a3ce4c6e..612fc32c01498d 100644 --- a/homeassistant/components/raincloud/manifest.json +++ b/homeassistant/components/raincloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "raincloud", "name": "Raincloud", - "documentation": "https://www.home-assistant.io/components/raincloud", + "documentation": "https://www.home-assistant.io/integrations/raincloud", "requirements": [ "raincloudy==0.0.7" ], diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index 354eaf8b313105..c5503976d3fbde 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -1,7 +1,7 @@ { "domain": "rainforest_eagle", "name": "Rainforest Eagle-200", - "documentation": "https://www.home-assistant.io/components/rainforest_eagle", + "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", "requirements": [ "eagle200_reader==0.2.1" ], diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 25b36c798c593d..37db2b31355261 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -2,7 +2,7 @@ "domain": "rainmachine", "name": "Rainmachine", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/rainmachine", + "documentation": "https://www.home-assistant.io/integrations/rainmachine", "requirements": [ "regenmaschine==1.5.1" ], diff --git a/homeassistant/components/random/manifest.json b/homeassistant/components/random/manifest.json index c184f35734c530..cbbaa562abde07 100644 --- a/homeassistant/components/random/manifest.json +++ b/homeassistant/components/random/manifest.json @@ -1,7 +1,7 @@ { "domain": "random", "name": "Random", - "documentation": "https://www.home-assistant.io/components/random", + "documentation": "https://www.home-assistant.io/integrations/random", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/raspihats/manifest.json b/homeassistant/components/raspihats/manifest.json index 8f5040152a2caf..b241cd6db15eeb 100644 --- a/homeassistant/components/raspihats/manifest.json +++ b/homeassistant/components/raspihats/manifest.json @@ -1,7 +1,7 @@ { "domain": "raspihats", "name": "Raspihats", - "documentation": "https://www.home-assistant.io/components/raspihats", + "documentation": "https://www.home-assistant.io/integrations/raspihats", "requirements": [ "raspihats==2.2.3", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/raspyrfm/manifest.json b/homeassistant/components/raspyrfm/manifest.json index fee815a7e6b17d..f1330c2abe56f2 100644 --- a/homeassistant/components/raspyrfm/manifest.json +++ b/homeassistant/components/raspyrfm/manifest.json @@ -1,7 +1,7 @@ { "domain": "raspyrfm", "name": "Raspyrfm", - "documentation": "https://www.home-assistant.io/components/raspyrfm", + "documentation": "https://www.home-assistant.io/integrations/raspyrfm", "requirements": [ "raspyrfm-client==1.2.8" ], diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index 2cccf32f298608..396afe30dcf074 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -1,7 +1,7 @@ { "domain": "recollect_waste", "name": "Recollect waste", - "documentation": "https://www.home-assistant.io/components/recollect_waste", + "documentation": "https://www.home-assistant.io/integrations/recollect_waste", "requirements": [ "recollect-waste==1.0.1" ], diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 9ecfa88053fa69..cdb09d66067170 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -1,7 +1,7 @@ { "domain": "recorder", "name": "Recorder", - "documentation": "https://www.home-assistant.io/components/recorder", + "documentation": "https://www.home-assistant.io/integrations/recorder", "requirements": [ "sqlalchemy==1.3.8" ], diff --git a/homeassistant/components/recswitch/manifest.json b/homeassistant/components/recswitch/manifest.json index af8e802c5ec2be..b11eca2b08822a 100644 --- a/homeassistant/components/recswitch/manifest.json +++ b/homeassistant/components/recswitch/manifest.json @@ -1,7 +1,7 @@ { "domain": "recswitch", "name": "Recswitch", - "documentation": "https://www.home-assistant.io/components/recswitch", + "documentation": "https://www.home-assistant.io/integrations/recswitch", "requirements": [ "pyrecswitch==1.0.2" ], diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index c6d3b3458e5287..55baecc486cdcb 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -1,7 +1,7 @@ { "domain": "reddit", "name": "Reddit", - "documentation": "https://www.home-assistant.io/components/reddit", + "documentation": "https://www.home-assistant.io/integrations/reddit", "requirements": [ "praw==6.3.1" ], diff --git a/homeassistant/components/rejseplanen/manifest.json b/homeassistant/components/rejseplanen/manifest.json index 7256239933006f..f1f8299918824f 100644 --- a/homeassistant/components/rejseplanen/manifest.json +++ b/homeassistant/components/rejseplanen/manifest.json @@ -1,7 +1,7 @@ { "domain": "rejseplanen", "name": "Rejseplanen", - "documentation": "https://www.home-assistant.io/components/rejseplanen", + "documentation": "https://www.home-assistant.io/integrations/rejseplanen", "requirements": [ "rjpl==0.3.5" ], diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index c9d35e9d2c9d7d..4979fe29e0eb74 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -1,7 +1,7 @@ { "domain": "remember_the_milk", "name": "Remember the milk", - "documentation": "https://www.home-assistant.io/components/remember_the_milk", + "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", "requirements": [ "RtmAPI==0.7.0", "httplib2==0.10.3" diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index 5fe585dcd8346d..5b9e70ebcf0cf6 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -1,7 +1,7 @@ { "domain": "remote", "name": "Remote", - "documentation": "https://www.home-assistant.io/components/remote", + "documentation": "https://www.home-assistant.io/integrations/remote", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/remote_rpi_gpio/manifest.json b/homeassistant/components/remote_rpi_gpio/manifest.json index f15defd63dcf3e..df9a9c7512358c 100644 --- a/homeassistant/components/remote_rpi_gpio/manifest.json +++ b/homeassistant/components/remote_rpi_gpio/manifest.json @@ -1,7 +1,7 @@ { "domain": "remote_rpi_gpio", "name": "remote_rpi_gpio", - "documentation": "https://www.home-assistant.io/components/remote_rpi_gpio", + "documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio", "requirements": [ "gpiozero==1.4.1" ], diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json index 14af98cfb641e6..a894285f729127 100644 --- a/homeassistant/components/repetier/manifest.json +++ b/homeassistant/components/repetier/manifest.json @@ -1,7 +1,7 @@ { "domain": "repetier", "name": "Repetier Server", - "documentation": "https://www.home-assistant.io/components/repetier", + "documentation": "https://www.home-assistant.io/integrations/repetier", "requirements": [ "pyrepetier==3.0.5" ], diff --git a/homeassistant/components/rest/manifest.json b/homeassistant/components/rest/manifest.json index 999f57407151d6..0b197d104eda0f 100644 --- a/homeassistant/components/rest/manifest.json +++ b/homeassistant/components/rest/manifest.json @@ -1,7 +1,7 @@ { "domain": "rest", "name": "Rest", - "documentation": "https://www.home-assistant.io/components/rest", + "documentation": "https://www.home-assistant.io/integrations/rest", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rest_command/manifest.json b/homeassistant/components/rest_command/manifest.json index ced930fc64f5e2..238191a9343a14 100644 --- a/homeassistant/components/rest_command/manifest.json +++ b/homeassistant/components/rest_command/manifest.json @@ -1,7 +1,7 @@ { "domain": "rest_command", "name": "Rest command", - "documentation": "https://www.home-assistant.io/components/rest_command", + "documentation": "https://www.home-assistant.io/integrations/rest_command", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index bbdb49ad401240..bda260bdff2230 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -1,7 +1,7 @@ { "domain": "rflink", "name": "Rflink", - "documentation": "https://www.home-assistant.io/components/rflink", + "documentation": "https://www.home-assistant.io/integrations/rflink", "requirements": [ "rflink==0.0.46" ], diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index 5d6cd4b038cbb8..a75a8ba9eb1d0c 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -1,7 +1,7 @@ { "domain": "rfxtrx", "name": "Rfxtrx", - "documentation": "https://www.home-assistant.io/components/rfxtrx", + "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "requirements": [ "pyRFXtrx==0.23" ], diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 9dbedad1ffc136..47fc2f3a6a8bbd 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -1,7 +1,7 @@ { "domain": "ring", "name": "Ring", - "documentation": "https://www.home-assistant.io/components/ring", + "documentation": "https://www.home-assistant.io/integrations/ring", "requirements": [ "ring_doorbell==0.2.3" ], diff --git a/homeassistant/components/ripple/manifest.json b/homeassistant/components/ripple/manifest.json index fe93bf02445dce..b8aa5c74302428 100644 --- a/homeassistant/components/ripple/manifest.json +++ b/homeassistant/components/ripple/manifest.json @@ -1,7 +1,7 @@ { "domain": "ripple", "name": "Ripple", - "documentation": "https://www.home-assistant.io/components/ripple", + "documentation": "https://www.home-assistant.io/integrations/ripple", "requirements": [ "python-ripple-api==0.0.3" ], diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index 3f32a61c081fc5..1f06daf0623f19 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -1,7 +1,7 @@ { "domain": "rmvtransport", "name": "Rmvtransport", - "documentation": "https://www.home-assistant.io/components/rmvtransport", + "documentation": "https://www.home-assistant.io/integrations/rmvtransport", "requirements": [ "PyRMVtransport==0.1.3" ], diff --git a/homeassistant/components/rocketchat/manifest.json b/homeassistant/components/rocketchat/manifest.json index 3a8959f1be61b5..924a9b86d47a9a 100644 --- a/homeassistant/components/rocketchat/manifest.json +++ b/homeassistant/components/rocketchat/manifest.json @@ -1,7 +1,7 @@ { "domain": "rocketchat", "name": "Rocketchat", - "documentation": "https://www.home-assistant.io/components/rocketchat", + "documentation": "https://www.home-assistant.io/integrations/rocketchat", "requirements": [ "rocketchat-API==0.6.1" ], diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 477bcb105f7f5b..f2639b31d1583a 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -1,7 +1,7 @@ { "domain": "roku", "name": "Roku", - "documentation": "https://www.home-assistant.io/components/roku", + "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": [ "roku==3.1" ], diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 058ad0c5e815ac..5064357a7df48a 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -1,7 +1,7 @@ { "domain": "roomba", "name": "Roomba", - "documentation": "https://www.home-assistant.io/components/roomba", + "documentation": "https://www.home-assistant.io/integrations/roomba", "requirements": [ "roombapy==1.3.1" ], diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 7ac91964a98720..34a296b0f9dece 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -1,7 +1,7 @@ { "domain": "route53", "name": "Route53", - "documentation": "https://www.home-assistant.io/components/route53", + "documentation": "https://www.home-assistant.io/integrations/route53", "requirements": [ "boto3==1.9.233", "ipify==1.0.0" diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index 71ec8fcbc9b3ab..e033e82d922029 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -1,7 +1,7 @@ { "domain": "rova", "name": "Rova", - "documentation": "https://www.home-assistant.io/components/rova", + "documentation": "https://www.home-assistant.io/integrations/rova", "requirements": [ "rova==0.1.0" ], diff --git a/homeassistant/components/rpi_camera/manifest.json b/homeassistant/components/rpi_camera/manifest.json index 1f905b103fed24..d5443787d39d53 100644 --- a/homeassistant/components/rpi_camera/manifest.json +++ b/homeassistant/components/rpi_camera/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_camera", "name": "Rpi camera", - "documentation": "https://www.home-assistant.io/components/rpi_camera", + "documentation": "https://www.home-assistant.io/integrations/rpi_camera", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json index 88322708b27738..0bee2baeddf741 100644 --- a/homeassistant/components/rpi_gpio/manifest.json +++ b/homeassistant/components/rpi_gpio/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_gpio", "name": "Rpi gpio", - "documentation": "https://www.home-assistant.io/components/rpi_gpio", + "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", "requirements": [ "RPi.GPIO==0.6.5" ], diff --git a/homeassistant/components/rpi_gpio_pwm/manifest.json b/homeassistant/components/rpi_gpio_pwm/manifest.json index d2ed380d68ad0b..ccff3d3357fdb0 100644 --- a/homeassistant/components/rpi_gpio_pwm/manifest.json +++ b/homeassistant/components/rpi_gpio_pwm/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_gpio_pwm", "name": "Rpi gpio pwm", - "documentation": "https://www.home-assistant.io/components/rpi_gpio_pwm", + "documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm", "requirements": [ "pwmled==1.4.1" ], diff --git a/homeassistant/components/rpi_pfio/manifest.json b/homeassistant/components/rpi_pfio/manifest.json index 7fc724bf90a3ef..fd0a0a99f58d49 100644 --- a/homeassistant/components/rpi_pfio/manifest.json +++ b/homeassistant/components/rpi_pfio/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_pfio", "name": "Rpi pfio", - "documentation": "https://www.home-assistant.io/components/rpi_pfio", + "documentation": "https://www.home-assistant.io/integrations/rpi_pfio", "requirements": [ "pifacecommon==4.2.2", "pifacedigitalio==3.0.5" diff --git a/homeassistant/components/rpi_rf/manifest.json b/homeassistant/components/rpi_rf/manifest.json index e5fffee131e337..5914f65ef6926d 100644 --- a/homeassistant/components/rpi_rf/manifest.json +++ b/homeassistant/components/rpi_rf/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_rf", "name": "Rpi rf", - "documentation": "https://www.home-assistant.io/components/rpi_rf", + "documentation": "https://www.home-assistant.io/integrations/rpi_rf", "requirements": [ "rpi-rf==0.9.7" ], diff --git a/homeassistant/components/rss_feed_template/manifest.json b/homeassistant/components/rss_feed_template/manifest.json index c92f6b2a0bad3a..7bd928764561a1 100644 --- a/homeassistant/components/rss_feed_template/manifest.json +++ b/homeassistant/components/rss_feed_template/manifest.json @@ -1,7 +1,7 @@ { "domain": "rss_feed_template", "name": "Rss feed template", - "documentation": "https://www.home-assistant.io/components/rss_feed_template", + "documentation": "https://www.home-assistant.io/integrations/rss_feed_template", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/rtorrent/manifest.json b/homeassistant/components/rtorrent/manifest.json index ce2dca9e0853d3..899ee3a8ae81cc 100644 --- a/homeassistant/components/rtorrent/manifest.json +++ b/homeassistant/components/rtorrent/manifest.json @@ -1,7 +1,7 @@ { "domain": "rtorrent", "name": "Rtorrent", - "documentation": "https://www.home-assistant.io/components/rtorrent", + "documentation": "https://www.home-assistant.io/integrations/rtorrent", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 4667e9b8314852..9404153f4e5132 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -1,7 +1,7 @@ { "domain": "russound_rio", "name": "Russound rio", - "documentation": "https://www.home-assistant.io/components/russound_rio", + "documentation": "https://www.home-assistant.io/integrations/russound_rio", "requirements": [ "russound_rio==0.1.7" ], diff --git a/homeassistant/components/russound_rnet/manifest.json b/homeassistant/components/russound_rnet/manifest.json index 716f383040f6f1..687dd05b61df73 100644 --- a/homeassistant/components/russound_rnet/manifest.json +++ b/homeassistant/components/russound_rnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "russound_rnet", "name": "Russound rnet", - "documentation": "https://www.home-assistant.io/components/russound_rnet", + "documentation": "https://www.home-assistant.io/integrations/russound_rnet", "requirements": [ "russound==0.1.9" ], diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 9424e5f3a1a591..e69c227f14884a 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -1,7 +1,7 @@ { "domain": "sabnzbd", "name": "Sabnzbd", - "documentation": "https://www.home-assistant.io/components/sabnzbd", + "documentation": "https://www.home-assistant.io/integrations/sabnzbd", "requirements": [ "pysabnzbd==1.1.0" ], diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index c0367c47902089..e42b37195a42de 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -1,7 +1,7 @@ { "domain": "saj", "name": "SAJ", - "documentation": "https://www.home-assistant.io/components/saj", + "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": [ "pysaj==0.0.9" ], diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index c8825f4ac3ff69..a080fac112a32d 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -1,7 +1,7 @@ { "domain": "samsungtv", "name": "Samsungtv", - "documentation": "https://www.home-assistant.io/components/samsungtv", + "documentation": "https://www.home-assistant.io/integrations/samsungtv", "requirements": [ "samsungctl[websocket]==0.7.1", "wakeonlan==1.1.6" diff --git a/homeassistant/components/satel_integra/manifest.json b/homeassistant/components/satel_integra/manifest.json index ae56b54ce18bbf..dbbfebeaccbc13 100644 --- a/homeassistant/components/satel_integra/manifest.json +++ b/homeassistant/components/satel_integra/manifest.json @@ -1,7 +1,7 @@ { "domain": "satel_integra", "name": "Satel integra", - "documentation": "https://www.home-assistant.io/components/satel_integra", + "documentation": "https://www.home-assistant.io/integrations/satel_integra", "requirements": [ "satel_integra==0.3.4" ], diff --git a/homeassistant/components/scene/manifest.json b/homeassistant/components/scene/manifest.json index e1becfd1936423..b80e4de5041642 100644 --- a/homeassistant/components/scene/manifest.json +++ b/homeassistant/components/scene/manifest.json @@ -1,7 +1,7 @@ { "domain": "scene", "name": "Scene", - "documentation": "https://www.home-assistant.io/components/scene", + "documentation": "https://www.home-assistant.io/integrations/scene", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index ec9807d4e004c7..989070900ca462 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -1,7 +1,7 @@ { "domain": "scrape", "name": "Scrape", - "documentation": "https://www.home-assistant.io/components/scrape", + "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": [ "beautifulsoup4==4.8.0" ], diff --git a/homeassistant/components/script/manifest.json b/homeassistant/components/script/manifest.json index 56a3c39b7b6b76..51ce17c500a041 100644 --- a/homeassistant/components/script/manifest.json +++ b/homeassistant/components/script/manifest.json @@ -1,7 +1,7 @@ { "domain": "script", "name": "Script", - "documentation": "https://www.home-assistant.io/components/script", + "documentation": "https://www.home-assistant.io/integrations/script", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/scsgate/manifest.json b/homeassistant/components/scsgate/manifest.json index d565a5d336d5d3..b6334272f23851 100644 --- a/homeassistant/components/scsgate/manifest.json +++ b/homeassistant/components/scsgate/manifest.json @@ -1,7 +1,7 @@ { "domain": "scsgate", "name": "Scsgate", - "documentation": "https://www.home-assistant.io/components/scsgate", + "documentation": "https://www.home-assistant.io/integrations/scsgate", "requirements": [ "scsgate==0.1.0" ], diff --git a/homeassistant/components/season/manifest.json b/homeassistant/components/season/manifest.json index 74bdb3d8b883a9..528e9ef35f1f12 100644 --- a/homeassistant/components/season/manifest.json +++ b/homeassistant/components/season/manifest.json @@ -1,7 +1,7 @@ { "domain": "season", "name": "Season", - "documentation": "https://www.home-assistant.io/components/season", + "documentation": "https://www.home-assistant.io/integrations/season", "requirements": [ "ephem==3.7.6.0" ], diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index 1ffbe69888f329..f9bd8cc31b4d5b 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -1,7 +1,7 @@ { "domain": "sendgrid", "name": "Sendgrid", - "documentation": "https://www.home-assistant.io/components/sendgrid", + "documentation": "https://www.home-assistant.io/integrations/sendgrid", "requirements": [ "sendgrid==6.1.0" ], diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 8763234c5ed64d..0112ca28469d66 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -1,7 +1,7 @@ { "domain": "sense", "name": "Sense", - "documentation": "https://www.home-assistant.io/components/sense", + "documentation": "https://www.home-assistant.io/integrations/sense", "requirements": [ "sense_energy==0.7.0" ], diff --git a/homeassistant/components/sensehat/manifest.json b/homeassistant/components/sensehat/manifest.json index cb148c92198875..deec8c23ab7f80 100644 --- a/homeassistant/components/sensehat/manifest.json +++ b/homeassistant/components/sensehat/manifest.json @@ -1,7 +1,7 @@ { "domain": "sensehat", "name": "Sensehat", - "documentation": "https://www.home-assistant.io/components/sensehat", + "documentation": "https://www.home-assistant.io/integrations/sensehat", "requirements": [ "sense-hat==2.2.0" ], diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 776b8444b82276..9b49f442d15560 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -1,7 +1,7 @@ { "domain": "sensibo", "name": "Sensibo", - "documentation": "https://www.home-assistant.io/components/sensibo", + "documentation": "https://www.home-assistant.io/integrations/sensibo", "requirements": [ "pysensibo==1.0.3" ], diff --git a/homeassistant/components/sensor/manifest.json b/homeassistant/components/sensor/manifest.json index 813bcc27acaf5d..1813d782d31a30 100644 --- a/homeassistant/components/sensor/manifest.json +++ b/homeassistant/components/sensor/manifest.json @@ -1,7 +1,7 @@ { "domain": "sensor", "name": "Sensor", - "documentation": "https://www.home-assistant.io/components/sensor", + "documentation": "https://www.home-assistant.io/integrations/sensor", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/serial/manifest.json b/homeassistant/components/serial/manifest.json index 945464dbdec027..61da7b4ff30cf4 100644 --- a/homeassistant/components/serial/manifest.json +++ b/homeassistant/components/serial/manifest.json @@ -1,7 +1,7 @@ { "domain": "serial", "name": "Serial", - "documentation": "https://www.home-assistant.io/components/serial", + "documentation": "https://www.home-assistant.io/integrations/serial", "requirements": [ "pyserial-asyncio==0.4" ], diff --git a/homeassistant/components/serial_pm/manifest.json b/homeassistant/components/serial_pm/manifest.json index b2a645c88f3c87..b326481c55b736 100644 --- a/homeassistant/components/serial_pm/manifest.json +++ b/homeassistant/components/serial_pm/manifest.json @@ -1,7 +1,7 @@ { "domain": "serial_pm", "name": "Serial pm", - "documentation": "https://www.home-assistant.io/components/serial_pm", + "documentation": "https://www.home-assistant.io/integrations/serial_pm", "requirements": [ "pmsensor==0.4" ], diff --git a/homeassistant/components/sesame/manifest.json b/homeassistant/components/sesame/manifest.json index ad6c71bd19fe9a..f689cd46858112 100644 --- a/homeassistant/components/sesame/manifest.json +++ b/homeassistant/components/sesame/manifest.json @@ -1,7 +1,7 @@ { "domain": "sesame", "name": "Sesame Smart Lock", - "documentation": "https://www.home-assistant.io/components/sesame", + "documentation": "https://www.home-assistant.io/integrations/sesame", "requirements": [ "pysesame2==1.0.1" ], diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 45ce2f6a7a00c2..0bf49d4b9066e7 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -1,7 +1,7 @@ { "domain": "seven_segments", "name": "Seven segments", - "documentation": "https://www.home-assistant.io/components/seven_segments", + "documentation": "https://www.home-assistant.io/integrations/seven_segments", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index 47b36c122913a2..6a5ac165291ec4 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -1,7 +1,7 @@ { "domain": "seventeentrack", "name": "Seventeentrack", - "documentation": "https://www.home-assistant.io/components/seventeentrack", + "documentation": "https://www.home-assistant.io/integrations/seventeentrack", "requirements": [ "py17track==2.2.2" ], diff --git a/homeassistant/components/shell_command/manifest.json b/homeassistant/components/shell_command/manifest.json index dfe9a8e8e6fb5b..ca354ff2331343 100644 --- a/homeassistant/components/shell_command/manifest.json +++ b/homeassistant/components/shell_command/manifest.json @@ -1,7 +1,7 @@ { "domain": "shell_command", "name": "Shell command", - "documentation": "https://www.home-assistant.io/components/shell_command", + "documentation": "https://www.home-assistant.io/integrations/shell_command", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/shiftr/manifest.json b/homeassistant/components/shiftr/manifest.json index 02718396e5e64c..282d86ce4195c4 100644 --- a/homeassistant/components/shiftr/manifest.json +++ b/homeassistant/components/shiftr/manifest.json @@ -1,7 +1,7 @@ { "domain": "shiftr", "name": "Shiftr", - "documentation": "https://www.home-assistant.io/components/shiftr", + "documentation": "https://www.home-assistant.io/integrations/shiftr", "requirements": [ "paho-mqtt==1.4.0" ], diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index fa704a6550a491..3ef01a44315a01 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -1,7 +1,7 @@ { "domain": "shodan", "name": "Shodan", - "documentation": "https://www.home-assistant.io/components/shodan", + "documentation": "https://www.home-assistant.io/integrations/shodan", "requirements": [ "shodan==1.19.0" ], diff --git a/homeassistant/components/shopping_list/manifest.json b/homeassistant/components/shopping_list/manifest.json index b4ea3c2d38816a..41fe28defaa6c3 100644 --- a/homeassistant/components/shopping_list/manifest.json +++ b/homeassistant/components/shopping_list/manifest.json @@ -1,7 +1,7 @@ { "domain": "shopping_list", "name": "Shopping list", - "documentation": "https://www.home-assistant.io/components/shopping_list", + "documentation": "https://www.home-assistant.io/integrations/shopping_list", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/sht31/manifest.json b/homeassistant/components/sht31/manifest.json index dfa22fc6e23b4a..6ed947e5c82c47 100644 --- a/homeassistant/components/sht31/manifest.json +++ b/homeassistant/components/sht31/manifest.json @@ -1,7 +1,7 @@ { "domain": "sht31", "name": "Sht31", - "documentation": "https://www.home-assistant.io/components/sht31", + "documentation": "https://www.home-assistant.io/integrations/sht31", "requirements": [ "Adafruit-GPIO==1.0.3", "Adafruit-SHT31==1.0.2" diff --git a/homeassistant/components/sigfox/manifest.json b/homeassistant/components/sigfox/manifest.json index 1dc8f5255cea1d..689703302a7953 100644 --- a/homeassistant/components/sigfox/manifest.json +++ b/homeassistant/components/sigfox/manifest.json @@ -1,7 +1,7 @@ { "domain": "sigfox", "name": "Sigfox", - "documentation": "https://www.home-assistant.io/components/sigfox", + "documentation": "https://www.home-assistant.io/integrations/sigfox", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index cbf2833a4f765b..d303ac221dd708 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -1,7 +1,7 @@ { "domain": "simplepush", "name": "Simplepush", - "documentation": "https://www.home-assistant.io/components/simplepush", + "documentation": "https://www.home-assistant.io/integrations/simplepush", "requirements": [ "simplepush==1.1.4" ], diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index cf26955b207b5e..96b337def55a6d 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -2,7 +2,7 @@ "domain": "simplisafe", "name": "Simplisafe", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/simplisafe", + "documentation": "https://www.home-assistant.io/integrations/simplisafe", "requirements": [ "simplisafe-python==5.0.1" ], diff --git a/homeassistant/components/simulated/manifest.json b/homeassistant/components/simulated/manifest.json index b972152aea4c9d..4dcd07157042e4 100644 --- a/homeassistant/components/simulated/manifest.json +++ b/homeassistant/components/simulated/manifest.json @@ -1,7 +1,7 @@ { "domain": "simulated", "name": "Simulated", - "documentation": "https://www.home-assistant.io/components/simulated", + "documentation": "https://www.home-assistant.io/integrations/simulated", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index f8e6e1bf14de9c..d5cfc48815130f 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -1,7 +1,7 @@ { "domain": "sisyphus", "name": "Sisyphus", - "documentation": "https://www.home-assistant.io/components/sisyphus", + "documentation": "https://www.home-assistant.io/integrations/sisyphus", "requirements": [ "sisyphus-control==2.2.1" ], diff --git a/homeassistant/components/sky_hub/manifest.json b/homeassistant/components/sky_hub/manifest.json index 46337918f84bcc..6be45690629415 100644 --- a/homeassistant/components/sky_hub/manifest.json +++ b/homeassistant/components/sky_hub/manifest.json @@ -1,7 +1,7 @@ { "domain": "sky_hub", "name": "Sky hub", - "documentation": "https://www.home-assistant.io/components/sky_hub", + "documentation": "https://www.home-assistant.io/integrations/sky_hub", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/skybeacon/manifest.json b/homeassistant/components/skybeacon/manifest.json index 893a1f3469e4f5..a3cb97cdc2db1f 100644 --- a/homeassistant/components/skybeacon/manifest.json +++ b/homeassistant/components/skybeacon/manifest.json @@ -1,7 +1,7 @@ { "domain": "skybeacon", "name": "Skybeacon", - "documentation": "https://www.home-assistant.io/components/skybeacon", + "documentation": "https://www.home-assistant.io/integrations/skybeacon", "requirements": [ "pygatt[GATTTOOL]==4.0.1" ], diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 843fd3d13b0b9f..8a005be52e29b6 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -1,7 +1,7 @@ { "domain": "skybell", "name": "Skybell", - "documentation": "https://www.home-assistant.io/components/skybell", + "documentation": "https://www.home-assistant.io/integrations/skybell", "requirements": [ "skybellpy==0.4.0" ], diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index 38fa5fa0894e01..10dc4bffccf581 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -1,7 +1,7 @@ { "domain": "slack", "name": "Slack", - "documentation": "https://www.home-assistant.io/components/slack", + "documentation": "https://www.home-assistant.io/integrations/slack", "requirements": [ "slacker==0.13.0" ], diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index ea16d626af4816..ac605f42b0c671 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -1,7 +1,7 @@ { "domain": "sleepiq", "name": "Sleepiq", - "documentation": "https://www.home-assistant.io/components/sleepiq", + "documentation": "https://www.home-assistant.io/integrations/sleepiq", "requirements": [ "sleepyq==0.7" ], diff --git a/homeassistant/components/slide/manifest.json b/homeassistant/components/slide/manifest.json index f9fd7f242b6332..3d2836e1809939 100644 --- a/homeassistant/components/slide/manifest.json +++ b/homeassistant/components/slide/manifest.json @@ -1,7 +1,7 @@ { "domain": "slide", "name": "Slide", - "documentation": "https://www.home-assistant.io/components/slide", + "documentation": "https://www.home-assistant.io/integrations/slide", "requirements": [ "goslide-api==0.5.1" ], diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index ea3a33d55ff818..8f0caa7c548ee0 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -1,7 +1,7 @@ { "domain": "sma", "name": "Sma", - "documentation": "https://www.home-assistant.io/components/sma", + "documentation": "https://www.home-assistant.io/integrations/sma", "requirements": [ "pysma==0.3.4" ], diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index 361802f312e46a..6f5a4b7b64bf8b 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -1,7 +1,7 @@ { "domain": "smappee", "name": "Smappee", - "documentation": "https://www.home-assistant.io/components/smappee", + "documentation": "https://www.home-assistant.io/integrations/smappee", "requirements": [ "smappy==0.2.16" ], diff --git a/homeassistant/components/smarthab/manifest.json b/homeassistant/components/smarthab/manifest.json index 18b587bac9280a..515f3dcbf651fe 100644 --- a/homeassistant/components/smarthab/manifest.json +++ b/homeassistant/components/smarthab/manifest.json @@ -1,7 +1,7 @@ { "domain": "smarthab", "name": "SmartHab", - "documentation": "https://www.home-assistant.io/components/smarthab", + "documentation": "https://www.home-assistant.io/integrations/smarthab", "requirements": [ "smarthab==0.20" ], diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 621da91f4f8c5f..8b5bf65afa1349 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -2,7 +2,7 @@ "domain": "smartthings", "name": "Smartthings", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/smartthings", + "documentation": "https://www.home-assistant.io/integrations/smartthings", "requirements": [ "pysmartapp==0.3.2", "pysmartthings==0.6.9" diff --git a/homeassistant/components/smarty/manifest.json b/homeassistant/components/smarty/manifest.json index b2e3deb4008c5f..003ee804b555ab 100644 --- a/homeassistant/components/smarty/manifest.json +++ b/homeassistant/components/smarty/manifest.json @@ -1,7 +1,7 @@ { "domain": "smarty", "name": "smarty", - "documentation": "https://www.home-assistant.io/components/smarty", + "documentation": "https://www.home-assistant.io/integrations/smarty", "requirements": [ "pysmarty==0.8" ], diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index 421eadca51caaa..b3fdc2825ae23e 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -2,7 +2,7 @@ "domain": "smhi", "name": "Smhi", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/smhi", + "documentation": "https://www.home-assistant.io/integrations/smhi", "requirements": [ "smhi-pkg==1.0.10" ], diff --git a/homeassistant/components/smtp/manifest.json b/homeassistant/components/smtp/manifest.json index 2e1a8d826ce53d..b6ec6b4ca0d4ab 100644 --- a/homeassistant/components/smtp/manifest.json +++ b/homeassistant/components/smtp/manifest.json @@ -1,7 +1,7 @@ { "domain": "smtp", "name": "Smtp", - "documentation": "https://www.home-assistant.io/components/smtp", + "documentation": "https://www.home-assistant.io/integrations/smtp", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json index 0bb36c1317aa79..1314b91f982a47 100644 --- a/homeassistant/components/snapcast/manifest.json +++ b/homeassistant/components/snapcast/manifest.json @@ -1,7 +1,7 @@ { "domain": "snapcast", "name": "Snapcast", - "documentation": "https://www.home-assistant.io/components/snapcast", + "documentation": "https://www.home-assistant.io/integrations/snapcast", "requirements": [ "snapcast==2.0.10" ], diff --git a/homeassistant/components/snips/manifest.json b/homeassistant/components/snips/manifest.json index 58fddb7a3f4ef4..e15d86e811f4ae 100644 --- a/homeassistant/components/snips/manifest.json +++ b/homeassistant/components/snips/manifest.json @@ -1,7 +1,7 @@ { "domain": "snips", "name": "Snips", - "documentation": "https://www.home-assistant.io/components/snips", + "documentation": "https://www.home-assistant.io/integrations/snips", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index a3ac3af985df86..d3942ab4a327f9 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -1,7 +1,7 @@ { "domain": "snmp", "name": "Snmp", - "documentation": "https://www.home-assistant.io/components/snmp", + "documentation": "https://www.home-assistant.io/integrations/snmp", "requirements": [ "pysnmp==4.4.11" ], diff --git a/homeassistant/components/sochain/manifest.json b/homeassistant/components/sochain/manifest.json index 23fad3683cb419..93361d41e2bcdb 100644 --- a/homeassistant/components/sochain/manifest.json +++ b/homeassistant/components/sochain/manifest.json @@ -1,7 +1,7 @@ { "domain": "sochain", "name": "Sochain", - "documentation": "https://www.home-assistant.io/components/sochain", + "documentation": "https://www.home-assistant.io/integrations/sochain", "requirements": [ "python-sochain-api==0.0.2" ], diff --git a/homeassistant/components/socialblade/manifest.json b/homeassistant/components/socialblade/manifest.json index e800bd7266a0ba..c90342018f8d2c 100644 --- a/homeassistant/components/socialblade/manifest.json +++ b/homeassistant/components/socialblade/manifest.json @@ -1,7 +1,7 @@ { "domain": "socialblade", "name": "Socialblade", - "documentation": "https://www.home-assistant.io/components/socialblade", + "documentation": "https://www.home-assistant.io/integrations/socialblade", "requirements": [ "socialbladeclient==0.2" ], diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index 7452790cd6043f..f2635936cb5169 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -1,7 +1,7 @@ { "domain": "solaredge", "name": "Solaredge", - "documentation": "https://www.home-assistant.io/components/solaredge", + "documentation": "https://www.home-assistant.io/integrations/solaredge", "requirements": [ "solaredge==0.0.2", "stringcase==1.2.0" diff --git a/homeassistant/components/solaredge_local/manifest.json b/homeassistant/components/solaredge_local/manifest.json index 291c774c383d56..91ed6f55ee13de 100644 --- a/homeassistant/components/solaredge_local/manifest.json +++ b/homeassistant/components/solaredge_local/manifest.json @@ -1,7 +1,7 @@ { "domain": "solaredge_local", "name": "Solar Edge Local", - "documentation": "https://www.home-assistant.io/components/solaredge_local", + "documentation": "https://www.home-assistant.io/integrations/solaredge_local", "requirements": [ "solaredge-local==0.2.0" ], diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 3a154b857fe878..14ac59d46214a1 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -1,7 +1,7 @@ { "domain": "solax", "name": "Solax Inverter", - "documentation": "https://www.home-assistant.io/components/solax", + "documentation": "https://www.home-assistant.io/integrations/solax", "requirements": [ "solax==0.2.2" ], diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index 02eab03c8bb31d..83b50684fda8be 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -2,7 +2,7 @@ "domain": "somfy", "name": "Somfy Open API", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/somfy", + "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": [], "codeowners": [ "@tetienne" diff --git a/homeassistant/components/somfy_mylink/manifest.json b/homeassistant/components/somfy_mylink/manifest.json index d4e799c2cf1ab6..35422cf09a1abb 100644 --- a/homeassistant/components/somfy_mylink/manifest.json +++ b/homeassistant/components/somfy_mylink/manifest.json @@ -1,7 +1,7 @@ { "domain": "somfy_mylink", "name": "Somfy MyLink", - "documentation": "https://www.home-assistant.io/components/somfy_mylink", + "documentation": "https://www.home-assistant.io/integrations/somfy_mylink", "requirements": [ "somfy-mylink-synergy==1.0.6" ], diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index bc0235ec5b3dbc..ae32083da39c8d 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -1,7 +1,7 @@ { "domain": "sonarr", "name": "Sonarr", - "documentation": "https://www.home-assistant.io/components/sonarr", + "documentation": "https://www.home-assistant.io/integrations/sonarr", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 5a01d9f9e2fa09..3160c4cee4b43a 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -1,7 +1,7 @@ { "domain": "songpal", "name": "Songpal", - "documentation": "https://www.home-assistant.io/components/songpal", + "documentation": "https://www.home-assistant.io/integrations/songpal", "requirements": [ "python-songpal==0.0.9.1" ], diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index a08c0a59c07fd6..6d636f36b3fb5f 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -2,7 +2,7 @@ "domain": "sonos", "name": "Sonos", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/sonos", + "documentation": "https://www.home-assistant.io/integrations/sonos", "requirements": [ "pysonos==0.0.23" ], diff --git a/homeassistant/components/sony_projector/manifest.json b/homeassistant/components/sony_projector/manifest.json index 1cc25d93f59863..497347671a9504 100644 --- a/homeassistant/components/sony_projector/manifest.json +++ b/homeassistant/components/sony_projector/manifest.json @@ -1,7 +1,7 @@ { "domain": "sony_projector", "name": "Sony projector", - "documentation": "https://www.home-assistant.io/components/sony_projector", + "documentation": "https://www.home-assistant.io/integrations/sony_projector", "requirements": [ "pysdcp==1" ], diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index eba60bc6e34693..e3ca9ed72e8814 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -1,7 +1,7 @@ { "domain": "soundtouch", "name": "Soundtouch", - "documentation": "https://www.home-assistant.io/components/soundtouch", + "documentation": "https://www.home-assistant.io/integrations/soundtouch", "requirements": [ "libsoundtouch==0.7.2" ], diff --git a/homeassistant/components/spaceapi/manifest.json b/homeassistant/components/spaceapi/manifest.json index 03aa5c0a1f7e2b..4ab05f170ca8a1 100644 --- a/homeassistant/components/spaceapi/manifest.json +++ b/homeassistant/components/spaceapi/manifest.json @@ -1,7 +1,7 @@ { "domain": "spaceapi", "name": "Spaceapi", - "documentation": "https://www.home-assistant.io/components/spaceapi", + "documentation": "https://www.home-assistant.io/integrations/spaceapi", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/spc/manifest.json b/homeassistant/components/spc/manifest.json index 572d4b04b87cf9..5b59d3bfe29e68 100644 --- a/homeassistant/components/spc/manifest.json +++ b/homeassistant/components/spc/manifest.json @@ -1,7 +1,7 @@ { "domain": "spc", "name": "Spc", - "documentation": "https://www.home-assistant.io/components/spc", + "documentation": "https://www.home-assistant.io/integrations/spc", "requirements": [ "pyspcwebgw==0.4.0" ], diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index 91b7e7c5c0fcff..b35df8ee7c8fbd 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "speedtestdotnet", "name": "Speedtestdotnet", - "documentation": "https://www.home-assistant.io/components/speedtestdotnet", + "documentation": "https://www.home-assistant.io/integrations/speedtestdotnet", "requirements": [ "speedtest-cli==2.1.1" ], diff --git a/homeassistant/components/spider/manifest.json b/homeassistant/components/spider/manifest.json index 4cd7a4677370a8..567f6f0e513e88 100644 --- a/homeassistant/components/spider/manifest.json +++ b/homeassistant/components/spider/manifest.json @@ -1,7 +1,7 @@ { "domain": "spider", "name": "Spider", - "documentation": "https://www.home-assistant.io/components/spider", + "documentation": "https://www.home-assistant.io/integrations/spider", "requirements": [ "spiderpy==1.3.1" ], diff --git a/homeassistant/components/splunk/manifest.json b/homeassistant/components/splunk/manifest.json index 2e81da3409a6fc..a6972e8881d3c3 100644 --- a/homeassistant/components/splunk/manifest.json +++ b/homeassistant/components/splunk/manifest.json @@ -1,7 +1,7 @@ { "domain": "splunk", "name": "Splunk", - "documentation": "https://www.home-assistant.io/components/splunk", + "documentation": "https://www.home-assistant.io/integrations/splunk", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/spotcrime/manifest.json b/homeassistant/components/spotcrime/manifest.json index 5827f307ecfc94..50d9ba3163d2bd 100644 --- a/homeassistant/components/spotcrime/manifest.json +++ b/homeassistant/components/spotcrime/manifest.json @@ -1,7 +1,7 @@ { "domain": "spotcrime", "name": "Spotcrime", - "documentation": "https://www.home-assistant.io/components/spotcrime", + "documentation": "https://www.home-assistant.io/integrations/spotcrime", "requirements": [ "spotcrime==1.0.4" ], diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 366a5eef0ad99e..f514a95c04205c 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -1,7 +1,7 @@ { "domain": "spotify", "name": "Spotify", - "documentation": "https://www.home-assistant.io/components/spotify", + "documentation": "https://www.home-assistant.io/integrations/spotify", "requirements": [ "spotipy-homeassistant==2.4.4.dev1" ], diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 38a320543a9cca..41d80ebccf9a03 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -1,7 +1,7 @@ { "domain": "sql", "name": "Sql", - "documentation": "https://www.home-assistant.io/components/sql", + "documentation": "https://www.home-assistant.io/integrations/sql", "requirements": [ "sqlalchemy==1.3.8" ], diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index ae124d6c03d517..1f651f746e6a3c 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "squeezebox", "name": "Squeezebox", - "documentation": "https://www.home-assistant.io/components/squeezebox", + "documentation": "https://www.home-assistant.io/integrations/squeezebox", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index ce00bcbc888e5a..1c3d56fe7fe9a7 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -1,7 +1,7 @@ { "domain": "ssdp", "name": "SSDP", - "documentation": "https://www.home-assistant.io/components/ssdp", + "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ "netdisco==2.6.0" ], diff --git a/homeassistant/components/starlingbank/manifest.json b/homeassistant/components/starlingbank/manifest.json index 1314fda5099fba..33dbb40f78a17b 100644 --- a/homeassistant/components/starlingbank/manifest.json +++ b/homeassistant/components/starlingbank/manifest.json @@ -1,7 +1,7 @@ { "domain": "starlingbank", "name": "Starlingbank", - "documentation": "https://www.home-assistant.io/components/starlingbank", + "documentation": "https://www.home-assistant.io/integrations/starlingbank", "requirements": [ "starlingbank==3.1" ], diff --git a/homeassistant/components/startca/manifest.json b/homeassistant/components/startca/manifest.json index d2f9e90c41a9dd..79637bfb124f89 100644 --- a/homeassistant/components/startca/manifest.json +++ b/homeassistant/components/startca/manifest.json @@ -1,7 +1,7 @@ { "domain": "startca", "name": "Startca", - "documentation": "https://www.home-assistant.io/components/startca", + "documentation": "https://www.home-assistant.io/integrations/startca", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/statistics/manifest.json b/homeassistant/components/statistics/manifest.json index 49e476a687632b..3dab05942b947b 100644 --- a/homeassistant/components/statistics/manifest.json +++ b/homeassistant/components/statistics/manifest.json @@ -1,7 +1,7 @@ { "domain": "statistics", "name": "Statistics", - "documentation": "https://www.home-assistant.io/components/statistics", + "documentation": "https://www.home-assistant.io/integrations/statistics", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/statsd/manifest.json b/homeassistant/components/statsd/manifest.json index 20f4cc7f5443a6..387576f8845fea 100644 --- a/homeassistant/components/statsd/manifest.json +++ b/homeassistant/components/statsd/manifest.json @@ -1,7 +1,7 @@ { "domain": "statsd", "name": "Statsd", - "documentation": "https://www.home-assistant.io/components/statsd", + "documentation": "https://www.home-assistant.io/integrations/statsd", "requirements": [ "statsd==3.2.1" ], diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index 735a1869c34d86..ecc6198cb88888 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -1,7 +1,7 @@ { "domain": "steam_online", "name": "Steam online", - "documentation": "https://www.home-assistant.io/components/steam_online", + "documentation": "https://www.home-assistant.io/integrations/steam_online", "requirements": [ "steamodd==4.21" ], diff --git a/homeassistant/components/stiebel_eltron/manifest.json b/homeassistant/components/stiebel_eltron/manifest.json index 0f8b586a9c2d6a..385df6a5063641 100644 --- a/homeassistant/components/stiebel_eltron/manifest.json +++ b/homeassistant/components/stiebel_eltron/manifest.json @@ -1,7 +1,7 @@ { "domain": "stiebel_eltron", "name": "STIEBEL ELTRON", - "documentation": "https://www.home-assistant.io/components/stiebel_eltron", + "documentation": "https://www.home-assistant.io/integrations/stiebel_eltron", "requirements": [ "pystiebeleltron==0.0.1.dev2" ], diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index f285f81f27fdac..a00315e1064dd3 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -1,7 +1,7 @@ { "domain": "stream", "name": "Stream", - "documentation": "https://www.home-assistant.io/components/stream", + "documentation": "https://www.home-assistant.io/integrations/stream", "requirements": [ "av==6.1.2" ], diff --git a/homeassistant/components/streamlabswater/manifest.json b/homeassistant/components/streamlabswater/manifest.json index b4173ebf0e9297..83b6314c4ab725 100644 --- a/homeassistant/components/streamlabswater/manifest.json +++ b/homeassistant/components/streamlabswater/manifest.json @@ -1,7 +1,7 @@ { "domain": "streamlabswater", "name": "Streamlabs Water", - "documentation": "https://www.home-assistant.io/components/streamlabswater", + "documentation": "https://www.home-assistant.io/integrations/streamlabswater", "requirements": [ "streamlabswater==1.0.1" ], diff --git a/homeassistant/components/stride/manifest.json b/homeassistant/components/stride/manifest.json index 307f4c929cfb4e..840984ad073a3f 100644 --- a/homeassistant/components/stride/manifest.json +++ b/homeassistant/components/stride/manifest.json @@ -1,7 +1,7 @@ { "domain": "stride", "name": "Stride", - "documentation": "https://www.home-assistant.io/components/stride", + "documentation": "https://www.home-assistant.io/integrations/stride", "requirements": [ "pystride==0.1.7" ], diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json index 8ee3de2d77fa34..654f99f7ea47b6 100644 --- a/homeassistant/components/suez_water/manifest.json +++ b/homeassistant/components/suez_water/manifest.json @@ -1,7 +1,7 @@ { "domain": "suez_water", "name": "Suez Water Consumption Sensor", - "documentation": "https://www.home-assistant.io/components/suez_water", + "documentation": "https://www.home-assistant.io/integrations/suez_water", "dependencies": [], "codeowners": ["@ooii"], "requirements": ["pysuez==0.1.17"] diff --git a/homeassistant/components/sun/manifest.json b/homeassistant/components/sun/manifest.json index e55131306dcea7..31fcb1d7c2e5d3 100644 --- a/homeassistant/components/sun/manifest.json +++ b/homeassistant/components/sun/manifest.json @@ -1,7 +1,7 @@ { "domain": "sun", "name": "Sun", - "documentation": "https://www.home-assistant.io/components/sun", + "documentation": "https://www.home-assistant.io/integrations/sun", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/supervisord/manifest.json b/homeassistant/components/supervisord/manifest.json index 1fc849165ef00c..eaf1e66cff454e 100644 --- a/homeassistant/components/supervisord/manifest.json +++ b/homeassistant/components/supervisord/manifest.json @@ -1,7 +1,7 @@ { "domain": "supervisord", "name": "Supervisord", - "documentation": "https://www.home-assistant.io/components/supervisord", + "documentation": "https://www.home-assistant.io/integrations/supervisord", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/supla/manifest.json b/homeassistant/components/supla/manifest.json index cac1a5f18abf20..72635ca641a303 100644 --- a/homeassistant/components/supla/manifest.json +++ b/homeassistant/components/supla/manifest.json @@ -1,7 +1,7 @@ { "domain": "supla", "name": "Supla", - "documentation": "https://www.home-assistant.io/components/supla", + "documentation": "https://www.home-assistant.io/integrations/supla", "requirements": [ "pysupla==0.0.3" ], diff --git a/homeassistant/components/swiss_hydrological_data/manifest.json b/homeassistant/components/swiss_hydrological_data/manifest.json index d6b18d6cba80a0..f54ef55ce17e96 100644 --- a/homeassistant/components/swiss_hydrological_data/manifest.json +++ b/homeassistant/components/swiss_hydrological_data/manifest.json @@ -1,7 +1,7 @@ { "domain": "swiss_hydrological_data", "name": "Swiss hydrological data", - "documentation": "https://www.home-assistant.io/components/swiss_hydrological_data", + "documentation": "https://www.home-assistant.io/integrations/swiss_hydrological_data", "requirements": [ "swisshydrodata==0.0.3" ], diff --git a/homeassistant/components/swiss_public_transport/manifest.json b/homeassistant/components/swiss_public_transport/manifest.json index 99dcdbd0c882c7..c91d85fecd7e11 100644 --- a/homeassistant/components/swiss_public_transport/manifest.json +++ b/homeassistant/components/swiss_public_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "swiss_public_transport", "name": "Swiss public transport", - "documentation": "https://www.home-assistant.io/components/swiss_public_transport", + "documentation": "https://www.home-assistant.io/integrations/swiss_public_transport", "requirements": [ "python_opendata_transport==0.1.4" ], diff --git a/homeassistant/components/swisscom/manifest.json b/homeassistant/components/swisscom/manifest.json index e52fda3408395f..27e33c81607e9a 100644 --- a/homeassistant/components/swisscom/manifest.json +++ b/homeassistant/components/swisscom/manifest.json @@ -1,7 +1,7 @@ { "domain": "swisscom", "name": "Swisscom", - "documentation": "https://www.home-assistant.io/components/swisscom", + "documentation": "https://www.home-assistant.io/integrations/swisscom", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/switch/manifest.json b/homeassistant/components/switch/manifest.json index 0f2872515827ac..73daca18c6a698 100644 --- a/homeassistant/components/switch/manifest.json +++ b/homeassistant/components/switch/manifest.json @@ -1,7 +1,7 @@ { "domain": "switch", "name": "Switch", - "documentation": "https://www.home-assistant.io/components/switch", + "documentation": "https://www.home-assistant.io/integrations/switch", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index b9ea4eb276e428..204a3605bb8860 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -1,7 +1,7 @@ { "domain": "switchbot", "name": "Switchbot", - "documentation": "https://www.home-assistant.io/components/switchbot", + "documentation": "https://www.home-assistant.io/integrations/switchbot", "requirements": [ "PySwitchbot==0.6.2" ], diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 2f3b3b6e84a5bc..453ae542b2cb5c 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -1,7 +1,7 @@ { "domain": "switcher_kis", "name": "Switcher", - "documentation": "https://www.home-assistant.io/components/switcher_kis/", + "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": [ "@tomerfi" ], diff --git a/homeassistant/components/switchmate/manifest.json b/homeassistant/components/switchmate/manifest.json index 94f100abe86513..b15024b545c9dd 100644 --- a/homeassistant/components/switchmate/manifest.json +++ b/homeassistant/components/switchmate/manifest.json @@ -1,7 +1,7 @@ { "domain": "switchmate", "name": "Switchmate", - "documentation": "https://www.home-assistant.io/components/switchmate", + "documentation": "https://www.home-assistant.io/integrations/switchmate", "requirements": [ "pySwitchmate==0.4.6" ], diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index a2a45826d9db0e..79e6f8e5571ce1 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -1,7 +1,7 @@ { "domain": "syncthru", "name": "Syncthru", - "documentation": "https://www.home-assistant.io/components/syncthru", + "documentation": "https://www.home-assistant.io/integrations/syncthru", "requirements": [ "pysyncthru==0.4.3" ], diff --git a/homeassistant/components/synology/manifest.json b/homeassistant/components/synology/manifest.json index a108f5fa98352b..ea743836c74464 100644 --- a/homeassistant/components/synology/manifest.json +++ b/homeassistant/components/synology/manifest.json @@ -1,7 +1,7 @@ { "domain": "synology", "name": "Synology", - "documentation": "https://www.home-assistant.io/components/synology", + "documentation": "https://www.home-assistant.io/integrations/synology", "requirements": [ "py-synology==0.2.0" ], diff --git a/homeassistant/components/synology_chat/manifest.json b/homeassistant/components/synology_chat/manifest.json index d35b1d8c902302..3522ba0405c846 100644 --- a/homeassistant/components/synology_chat/manifest.json +++ b/homeassistant/components/synology_chat/manifest.json @@ -1,7 +1,7 @@ { "domain": "synology_chat", "name": "Synology chat", - "documentation": "https://www.home-assistant.io/components/synology_chat", + "documentation": "https://www.home-assistant.io/integrations/synology_chat", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/synology_srm/manifest.json b/homeassistant/components/synology_srm/manifest.json index a790a6c453cd1d..c507e07c717bc5 100644 --- a/homeassistant/components/synology_srm/manifest.json +++ b/homeassistant/components/synology_srm/manifest.json @@ -1,7 +1,7 @@ { "domain": "synology_srm", "name": "Synology SRM", - "documentation": "https://www.home-assistant.io/components/synology_srm", + "documentation": "https://www.home-assistant.io/integrations/synology_srm", "requirements": [ "synology-srm==0.0.7" ], diff --git a/homeassistant/components/synologydsm/manifest.json b/homeassistant/components/synologydsm/manifest.json index fcce2e52a215d4..d7dc76b6ac917c 100644 --- a/homeassistant/components/synologydsm/manifest.json +++ b/homeassistant/components/synologydsm/manifest.json @@ -1,7 +1,7 @@ { "domain": "synologydsm", "name": "Synologydsm", - "documentation": "https://www.home-assistant.io/components/synologydsm", + "documentation": "https://www.home-assistant.io/integrations/synologydsm", "requirements": [ "python-synology==0.2.0" ], diff --git a/homeassistant/components/syslog/manifest.json b/homeassistant/components/syslog/manifest.json index 19836ffa67f094..00dda3ebc02b39 100644 --- a/homeassistant/components/syslog/manifest.json +++ b/homeassistant/components/syslog/manifest.json @@ -1,7 +1,7 @@ { "domain": "syslog", "name": "Syslog", - "documentation": "https://www.home-assistant.io/components/syslog", + "documentation": "https://www.home-assistant.io/integrations/syslog", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/system_health/manifest.json b/homeassistant/components/system_health/manifest.json index 9c2b7bcae39c2e..4de7af3f8628d7 100644 --- a/homeassistant/components/system_health/manifest.json +++ b/homeassistant/components/system_health/manifest.json @@ -1,7 +1,7 @@ { "domain": "system_health", "name": "System health", - "documentation": "https://www.home-assistant.io/components/system_health", + "documentation": "https://www.home-assistant.io/integrations/system_health", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/system_log/manifest.json b/homeassistant/components/system_log/manifest.json index 01f70af4a15c36..0bc14a56e71dec 100644 --- a/homeassistant/components/system_log/manifest.json +++ b/homeassistant/components/system_log/manifest.json @@ -1,7 +1,7 @@ { "domain": "system_log", "name": "System log", - "documentation": "https://www.home-assistant.io/components/system_log", + "documentation": "https://www.home-assistant.io/integrations/system_log", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 565a459818fd14..a45a59e410a087 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "systemmonitor", "name": "Systemmonitor", - "documentation": "https://www.home-assistant.io/components/systemmonitor", + "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "requirements": [ "psutil==5.6.3" ], diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 8d42cde1c05162..9a884fa010c4a3 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -1,7 +1,7 @@ { "domain": "tado", "name": "Tado", - "documentation": "https://www.home-assistant.io/components/tado", + "documentation": "https://www.home-assistant.io/integrations/tado", "requirements": [ "python-tado==0.2.9" ], diff --git a/homeassistant/components/tahoma/manifest.json b/homeassistant/components/tahoma/manifest.json index ca3ab0bc882e21..1e99d4b288d725 100644 --- a/homeassistant/components/tahoma/manifest.json +++ b/homeassistant/components/tahoma/manifest.json @@ -1,7 +1,7 @@ { "domain": "tahoma", "name": "Tahoma", - "documentation": "https://www.home-assistant.io/components/tahoma", + "documentation": "https://www.home-assistant.io/integrations/tahoma", "requirements": [ "tahoma-api==0.0.14" ], diff --git a/homeassistant/components/tank_utility/manifest.json b/homeassistant/components/tank_utility/manifest.json index 04ffb48f39656c..328285ab67b599 100644 --- a/homeassistant/components/tank_utility/manifest.json +++ b/homeassistant/components/tank_utility/manifest.json @@ -1,7 +1,7 @@ { "domain": "tank_utility", "name": "Tank utility", - "documentation": "https://www.home-assistant.io/components/tank_utility", + "documentation": "https://www.home-assistant.io/integrations/tank_utility", "requirements": [ "tank_utility==1.4.0" ], diff --git a/homeassistant/components/tapsaff/manifest.json b/homeassistant/components/tapsaff/manifest.json index 1c8e8476987b25..79fff1b2b4c2b1 100644 --- a/homeassistant/components/tapsaff/manifest.json +++ b/homeassistant/components/tapsaff/manifest.json @@ -1,7 +1,7 @@ { "domain": "tapsaff", "name": "Tapsaff", - "documentation": "https://www.home-assistant.io/components/tapsaff", + "documentation": "https://www.home-assistant.io/integrations/tapsaff", "requirements": [ "tapsaff==0.2.1" ], diff --git a/homeassistant/components/tautulli/manifest.json b/homeassistant/components/tautulli/manifest.json index d49b52801813d8..cc635082c35b5c 100644 --- a/homeassistant/components/tautulli/manifest.json +++ b/homeassistant/components/tautulli/manifest.json @@ -1,7 +1,7 @@ { "domain": "tautulli", "name": "Tautulli", - "documentation": "https://www.home-assistant.io/components/tautulli", + "documentation": "https://www.home-assistant.io/integrations/tautulli", "requirements": [ "pytautulli==0.5.0" ], diff --git a/homeassistant/components/tcp/manifest.json b/homeassistant/components/tcp/manifest.json index 2ff29a27f3169b..53f3f6c64636d7 100644 --- a/homeassistant/components/tcp/manifest.json +++ b/homeassistant/components/tcp/manifest.json @@ -1,7 +1,7 @@ { "domain": "tcp", "name": "Tcp", - "documentation": "https://www.home-assistant.io/components/tcp", + "documentation": "https://www.home-assistant.io/integrations/tcp", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ted5000/manifest.json b/homeassistant/components/ted5000/manifest.json index 9cc50405bad9c0..b02a9db3fdc639 100644 --- a/homeassistant/components/ted5000/manifest.json +++ b/homeassistant/components/ted5000/manifest.json @@ -1,7 +1,7 @@ { "domain": "ted5000", "name": "Ted5000", - "documentation": "https://www.home-assistant.io/components/ted5000", + "documentation": "https://www.home-assistant.io/integrations/ted5000", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/teksavvy/manifest.json b/homeassistant/components/teksavvy/manifest.json index 14afdec3b71551..220a086e0be91d 100644 --- a/homeassistant/components/teksavvy/manifest.json +++ b/homeassistant/components/teksavvy/manifest.json @@ -1,7 +1,7 @@ { "domain": "teksavvy", "name": "Teksavvy", - "documentation": "https://www.home-assistant.io/components/teksavvy", + "documentation": "https://www.home-assistant.io/integrations/teksavvy", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/telegram/manifest.json b/homeassistant/components/telegram/manifest.json index 8a6dd7fb369d4c..392c45ea886dbd 100644 --- a/homeassistant/components/telegram/manifest.json +++ b/homeassistant/components/telegram/manifest.json @@ -1,7 +1,7 @@ { "domain": "telegram", "name": "Telegram", - "documentation": "https://www.home-assistant.io/components/telegram", + "documentation": "https://www.home-assistant.io/integrations/telegram", "requirements": [], "dependencies": [ "telegram_bot" diff --git a/homeassistant/components/telegram_bot/manifest.json b/homeassistant/components/telegram_bot/manifest.json index f341fd587ca0e1..afc83879cb0c1e 100644 --- a/homeassistant/components/telegram_bot/manifest.json +++ b/homeassistant/components/telegram_bot/manifest.json @@ -1,7 +1,7 @@ { "domain": "telegram_bot", "name": "Telegram bot", - "documentation": "https://www.home-assistant.io/components/telegram_bot", + "documentation": "https://www.home-assistant.io/integrations/telegram_bot", "requirements": [ "python-telegram-bot==11.1.0" ], diff --git a/homeassistant/components/tellduslive/manifest.json b/homeassistant/components/tellduslive/manifest.json index 7f431ba92b1b61..777138d44ab6a8 100644 --- a/homeassistant/components/tellduslive/manifest.json +++ b/homeassistant/components/tellduslive/manifest.json @@ -2,7 +2,7 @@ "domain": "tellduslive", "name": "Tellduslive", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/tellduslive", + "documentation": "https://www.home-assistant.io/integrations/tellduslive", "requirements": [ "tellduslive==0.10.10" ], diff --git a/homeassistant/components/tellstick/manifest.json b/homeassistant/components/tellstick/manifest.json index c50ba514f2aaab..3391533b081012 100644 --- a/homeassistant/components/tellstick/manifest.json +++ b/homeassistant/components/tellstick/manifest.json @@ -1,7 +1,7 @@ { "domain": "tellstick", "name": "Tellstick", - "documentation": "https://www.home-assistant.io/components/tellstick", + "documentation": "https://www.home-assistant.io/integrations/tellstick", "requirements": [ "tellcore-net==0.4", "tellcore-py==1.1.2" diff --git a/homeassistant/components/telnet/manifest.json b/homeassistant/components/telnet/manifest.json index 58f5e15cc1a37a..afba0e38301c90 100644 --- a/homeassistant/components/telnet/manifest.json +++ b/homeassistant/components/telnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "telnet", "name": "Telnet", - "documentation": "https://www.home-assistant.io/components/telnet", + "documentation": "https://www.home-assistant.io/integrations/telnet", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/temper/manifest.json b/homeassistant/components/temper/manifest.json index 0e60c957d9d6a9..76656e9936fdd9 100644 --- a/homeassistant/components/temper/manifest.json +++ b/homeassistant/components/temper/manifest.json @@ -1,7 +1,7 @@ { "domain": "temper", "name": "Temper", - "documentation": "https://www.home-assistant.io/components/temper", + "documentation": "https://www.home-assistant.io/integrations/temper", "requirements": [ "temperusb==1.5.3" ], diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json index c8406c9d08494c..20a35f1afe75a3 100644 --- a/homeassistant/components/template/manifest.json +++ b/homeassistant/components/template/manifest.json @@ -1,7 +1,7 @@ { "domain": "template", "name": "Template", - "documentation": "https://www.home-assistant.io/components/template", + "documentation": "https://www.home-assistant.io/integrations/template", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 279ac3b103cba8..e7d35829ffb2fb 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -1,7 +1,7 @@ { "domain": "tensorflow", "name": "Tensorflow", - "documentation": "https://www.home-assistant.io/components/tensorflow", + "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", "numpy==1.17.1", diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index ab32a64e670f25..4071178c7c3ae0 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -1,7 +1,7 @@ { "domain": "tesla", "name": "Tesla", - "documentation": "https://www.home-assistant.io/components/tesla", + "documentation": "https://www.home-assistant.io/integrations/tesla", "requirements": [ "teslajsonpy==0.0.25" ], diff --git a/homeassistant/components/tfiac/manifest.json b/homeassistant/components/tfiac/manifest.json index d7282317d95dfb..7c93000790cc2c 100644 --- a/homeassistant/components/tfiac/manifest.json +++ b/homeassistant/components/tfiac/manifest.json @@ -1,7 +1,7 @@ { "domain": "tfiac", "name": "Tfiac", - "documentation": "https://www.home-assistant.io/components/tfiac", + "documentation": "https://www.home-assistant.io/integrations/tfiac", "requirements": [ "pytfiac==0.4" ], diff --git a/homeassistant/components/thermoworks_smoke/manifest.json b/homeassistant/components/thermoworks_smoke/manifest.json index fab670627ba891..93a334fc986b0a 100644 --- a/homeassistant/components/thermoworks_smoke/manifest.json +++ b/homeassistant/components/thermoworks_smoke/manifest.json @@ -1,7 +1,7 @@ { "domain": "thermoworks_smoke", "name": "Thermoworks smoke", - "documentation": "https://www.home-assistant.io/components/thermoworks_smoke", + "documentation": "https://www.home-assistant.io/integrations/thermoworks_smoke", "requirements": [ "stringcase==1.2.0", "thermoworks_smoke==0.1.8" diff --git a/homeassistant/components/thethingsnetwork/manifest.json b/homeassistant/components/thethingsnetwork/manifest.json index 8d6082d74bfbb8..98a852dea08a0b 100644 --- a/homeassistant/components/thethingsnetwork/manifest.json +++ b/homeassistant/components/thethingsnetwork/manifest.json @@ -1,7 +1,7 @@ { "domain": "thethingsnetwork", "name": "Thethingsnetwork", - "documentation": "https://www.home-assistant.io/components/thethingsnetwork", + "documentation": "https://www.home-assistant.io/integrations/thethingsnetwork", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/thingspeak/manifest.json b/homeassistant/components/thingspeak/manifest.json index 482bb94ac2ae0a..689a6678cab505 100644 --- a/homeassistant/components/thingspeak/manifest.json +++ b/homeassistant/components/thingspeak/manifest.json @@ -1,7 +1,7 @@ { "domain": "thingspeak", "name": "Thingspeak", - "documentation": "https://www.home-assistant.io/components/thingspeak", + "documentation": "https://www.home-assistant.io/integrations/thingspeak", "requirements": [ "thingspeak==0.4.1" ], diff --git a/homeassistant/components/thinkingcleaner/manifest.json b/homeassistant/components/thinkingcleaner/manifest.json index 4e43270a5e04ef..c7c8aaaf16a65e 100644 --- a/homeassistant/components/thinkingcleaner/manifest.json +++ b/homeassistant/components/thinkingcleaner/manifest.json @@ -1,7 +1,7 @@ { "domain": "thinkingcleaner", "name": "Thinkingcleaner", - "documentation": "https://www.home-assistant.io/components/thinkingcleaner", + "documentation": "https://www.home-assistant.io/integrations/thinkingcleaner", "requirements": [ "pythinkingcleaner==0.0.3" ], diff --git a/homeassistant/components/thomson/manifest.json b/homeassistant/components/thomson/manifest.json index 063c84d4ff7e8f..ac07a2f77ad5d9 100644 --- a/homeassistant/components/thomson/manifest.json +++ b/homeassistant/components/thomson/manifest.json @@ -1,7 +1,7 @@ { "domain": "thomson", "name": "Thomson", - "documentation": "https://www.home-assistant.io/components/thomson", + "documentation": "https://www.home-assistant.io/integrations/thomson", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/threshold/manifest.json b/homeassistant/components/threshold/manifest.json index 107b4351505a7f..f3aa54052789de 100644 --- a/homeassistant/components/threshold/manifest.json +++ b/homeassistant/components/threshold/manifest.json @@ -1,7 +1,7 @@ { "domain": "threshold", "name": "Threshold", - "documentation": "https://www.home-assistant.io/components/threshold", + "documentation": "https://www.home-assistant.io/integrations/threshold", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index d0f358c5902bec..11c5a676bf6b5c 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -1,7 +1,7 @@ { "domain": "tibber", "name": "Tibber", - "documentation": "https://www.home-assistant.io/components/tibber", + "documentation": "https://www.home-assistant.io/integrations/tibber", "requirements": [ "pyTibber==0.11.7" ], diff --git a/homeassistant/components/tikteck/manifest.json b/homeassistant/components/tikteck/manifest.json index 7edaf9ba978bff..c81b6f0a0b1f17 100644 --- a/homeassistant/components/tikteck/manifest.json +++ b/homeassistant/components/tikteck/manifest.json @@ -1,7 +1,7 @@ { "domain": "tikteck", "name": "Tikteck", - "documentation": "https://www.home-assistant.io/components/tikteck", + "documentation": "https://www.home-assistant.io/integrations/tikteck", "requirements": [ "tikteck==0.4" ], diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 3d26e8315ae4b6..5e40c89369ac14 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -1,7 +1,7 @@ { "domain": "tile", "name": "Tile", - "documentation": "https://www.home-assistant.io/components/tile", + "documentation": "https://www.home-assistant.io/integrations/tile", "requirements": [ "pytile==2.0.6" ], diff --git a/homeassistant/components/time_date/manifest.json b/homeassistant/components/time_date/manifest.json index bd620d4a18fc55..1824ea2db41b39 100644 --- a/homeassistant/components/time_date/manifest.json +++ b/homeassistant/components/time_date/manifest.json @@ -1,7 +1,7 @@ { "domain": "time_date", "name": "Time date", - "documentation": "https://www.home-assistant.io/components/time_date", + "documentation": "https://www.home-assistant.io/integrations/time_date", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/timer/manifest.json b/homeassistant/components/timer/manifest.json index 76a506faee86fb..1cf2c610014695 100644 --- a/homeassistant/components/timer/manifest.json +++ b/homeassistant/components/timer/manifest.json @@ -1,7 +1,7 @@ { "domain": "timer", "name": "Timer", - "documentation": "https://www.home-assistant.io/components/timer", + "documentation": "https://www.home-assistant.io/integrations/timer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/tod/manifest.json b/homeassistant/components/tod/manifest.json index ff67748d64cd04..ff582b33cefa9f 100644 --- a/homeassistant/components/tod/manifest.json +++ b/homeassistant/components/tod/manifest.json @@ -1,7 +1,7 @@ { "domain": "tod", "name": "Tod", - "documentation": "https://www.home-assistant.io/components/tod", + "documentation": "https://www.home-assistant.io/integrations/tod", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index 7a6b4e2efab7a9..dbf1a941e00ba3 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -1,7 +1,7 @@ { "domain": "todoist", "name": "Todoist", - "documentation": "https://www.home-assistant.io/components/todoist", + "documentation": "https://www.home-assistant.io/integrations/todoist", "requirements": [ "todoist-python==7.0.17" ], diff --git a/homeassistant/components/tof/manifest.json b/homeassistant/components/tof/manifest.json index 5f64e661a9a5f4..dc87b6c2fc77ae 100644 --- a/homeassistant/components/tof/manifest.json +++ b/homeassistant/components/tof/manifest.json @@ -1,7 +1,7 @@ { "domain": "tof", "name": "Tof", - "documentation": "https://www.home-assistant.io/components/tof", + "documentation": "https://www.home-assistant.io/integrations/tof", "requirements": [ "VL53L1X2==0.1.5" ], diff --git a/homeassistant/components/tomato/manifest.json b/homeassistant/components/tomato/manifest.json index 615ea9ecd7eaa7..5f6584ce2501d6 100644 --- a/homeassistant/components/tomato/manifest.json +++ b/homeassistant/components/tomato/manifest.json @@ -1,7 +1,7 @@ { "domain": "tomato", "name": "Tomato", - "documentation": "https://www.home-assistant.io/components/tomato", + "documentation": "https://www.home-assistant.io/integrations/tomato", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/toon/manifest.json b/homeassistant/components/toon/manifest.json index 3fd00e88a0c16f..721f7037fce28c 100644 --- a/homeassistant/components/toon/manifest.json +++ b/homeassistant/components/toon/manifest.json @@ -2,7 +2,7 @@ "domain": "toon", "name": "Toon", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/toon", + "documentation": "https://www.home-assistant.io/integrations/toon", "requirements": [ "toonapilib==3.2.4" ], diff --git a/homeassistant/components/torque/manifest.json b/homeassistant/components/torque/manifest.json index 9ce41b59861a69..fbc788d252d125 100644 --- a/homeassistant/components/torque/manifest.json +++ b/homeassistant/components/torque/manifest.json @@ -1,7 +1,7 @@ { "domain": "torque", "name": "Torque", - "documentation": "https://www.home-assistant.io/components/torque", + "documentation": "https://www.home-assistant.io/integrations/torque", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index e6bcd6dd00a3ae..39cd0e0f1e6ca3 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -1,7 +1,7 @@ { "domain": "totalconnect", "name": "Totalconnect", - "documentation": "https://www.home-assistant.io/components/totalconnect", + "documentation": "https://www.home-assistant.io/integrations/totalconnect", "requirements": [ "total_connect_client==0.28" ], diff --git a/homeassistant/components/touchline/manifest.json b/homeassistant/components/touchline/manifest.json index 5b8b4f521ee268..7c0b36b50fe8dd 100644 --- a/homeassistant/components/touchline/manifest.json +++ b/homeassistant/components/touchline/manifest.json @@ -1,7 +1,7 @@ { "domain": "touchline", "name": "Touchline", - "documentation": "https://www.home-assistant.io/components/touchline", + "documentation": "https://www.home-assistant.io/integrations/touchline", "requirements": [ "pytouchline==0.7" ], diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index e0f85757afda5f..f299e02e2d3406 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -2,7 +2,7 @@ "domain": "tplink", "name": "Tplink", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/tplink", + "documentation": "https://www.home-assistant.io/integrations/tplink", "requirements": [ "pyHS100==0.3.5", "tplink==0.2.1" diff --git a/homeassistant/components/tplink_lte/manifest.json b/homeassistant/components/tplink_lte/manifest.json index e3efd8c83310a3..e1cdacde6d8cf4 100644 --- a/homeassistant/components/tplink_lte/manifest.json +++ b/homeassistant/components/tplink_lte/manifest.json @@ -1,7 +1,7 @@ { "domain": "tplink_lte", "name": "Tplink lte", - "documentation": "https://www.home-assistant.io/components/tplink_lte", + "documentation": "https://www.home-assistant.io/integrations/tplink_lte", "requirements": [ "tp-connected==0.0.4" ], diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 7d3e2f22d65aab..ffc82ee13e8ae8 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -2,7 +2,7 @@ "domain": "traccar", "name": "Traccar", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/traccar", + "documentation": "https://www.home-assistant.io/integrations/traccar", "requirements": [ "pytraccar==0.9.0", "stringcase==1.2.0" diff --git a/homeassistant/components/trackr/manifest.json b/homeassistant/components/trackr/manifest.json index 6ad348176ba11c..638d63cb487ab7 100644 --- a/homeassistant/components/trackr/manifest.json +++ b/homeassistant/components/trackr/manifest.json @@ -1,7 +1,7 @@ { "domain": "trackr", "name": "Trackr", - "documentation": "https://www.home-assistant.io/components/trackr", + "documentation": "https://www.home-assistant.io/integrations/trackr", "requirements": [ "pytrackr==0.0.5" ], diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index d847c6df24f7cb..d9fa4ad5696b8e 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -2,7 +2,7 @@ "domain": "tradfri", "name": "Tradfri", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/tradfri", + "documentation": "https://www.home-assistant.io/integrations/tradfri", "requirements": ["pytradfri[async]==6.3.1"], "homekit": { "models": ["TRADFRI"] diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 2e24100edd0b8f..1e24895af44b2e 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -1,7 +1,7 @@ { "domain": "trafikverket_train", "name": "Trafikverket train information", - "documentation": "https://www.home-assistant.io/components/trafikverket_train", + "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", "requirements": [ "pytrafikverket==0.1.5.9" ], diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index 9bd734fe094065..64f1d636a1a979 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -1,7 +1,7 @@ { "domain": "trafikverket_weatherstation", "name": "Trafikverket weatherstation", - "documentation": "https://www.home-assistant.io/components/trafikverket_weatherstation", + "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", "requirements": [ "pytrafikverket==0.1.5.9" ], diff --git a/homeassistant/components/transmission/manifest.json b/homeassistant/components/transmission/manifest.json index 2bd4571ef93b92..c2fa31d7b500c9 100644 --- a/homeassistant/components/transmission/manifest.json +++ b/homeassistant/components/transmission/manifest.json @@ -2,7 +2,7 @@ "domain": "transmission", "name": "Transmission", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/transmission", + "documentation": "https://www.home-assistant.io/integrations/transmission", "requirements": [ "transmissionrpc==0.11" ], diff --git a/homeassistant/components/transport_nsw/manifest.json b/homeassistant/components/transport_nsw/manifest.json index 491cce7407f861..9d2e675c75712f 100644 --- a/homeassistant/components/transport_nsw/manifest.json +++ b/homeassistant/components/transport_nsw/manifest.json @@ -1,7 +1,7 @@ { "domain": "transport_nsw", "name": "Transport nsw", - "documentation": "https://www.home-assistant.io/components/transport_nsw", + "documentation": "https://www.home-assistant.io/integrations/transport_nsw", "requirements": [ "PyTransportNSW==0.1.1" ], diff --git a/homeassistant/components/travisci/manifest.json b/homeassistant/components/travisci/manifest.json index eb553fbe73c38f..f0d5d54c8cfd5c 100644 --- a/homeassistant/components/travisci/manifest.json +++ b/homeassistant/components/travisci/manifest.json @@ -1,7 +1,7 @@ { "domain": "travisci", "name": "Travisci", - "documentation": "https://www.home-assistant.io/components/travisci", + "documentation": "https://www.home-assistant.io/integrations/travisci", "requirements": [ "TravisPy==0.3.5" ], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 8719138f3ac213..1432d2d21a0662 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -1,7 +1,7 @@ { "domain": "trend", "name": "Trend", - "documentation": "https://www.home-assistant.io/components/trend", + "documentation": "https://www.home-assistant.io/integrations/trend", "requirements": [ "numpy==1.17.1" ], diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index ce600473cc5429..ca2059a4d19e8f 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -1,7 +1,7 @@ { "domain": "tts", "name": "Tts", - "documentation": "https://www.home-assistant.io/components/tts", + "documentation": "https://www.home-assistant.io/integrations/tts", "requirements": [ "mutagen==1.42.0" ], diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 9c83056f6aca81..cf16d587e87709 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -1,7 +1,7 @@ { "domain": "tuya", "name": "Tuya", - "documentation": "https://www.home-assistant.io/components/tuya", + "documentation": "https://www.home-assistant.io/integrations/tuya", "requirements": [ "tuyaha==0.0.4" ], diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index d1acf936a243ad..4eebba28ef67ed 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -2,7 +2,7 @@ "domain": "twentemilieu", "name": "Twente Milieu", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/twentemilieu", + "documentation": "https://www.home-assistant.io/integrations/twentemilieu", "requirements": [ "twentemilieu==0.1.0" ], diff --git a/homeassistant/components/twilio/manifest.json b/homeassistant/components/twilio/manifest.json index f96afa18115f0e..23fac51a3476b2 100644 --- a/homeassistant/components/twilio/manifest.json +++ b/homeassistant/components/twilio/manifest.json @@ -2,7 +2,7 @@ "domain": "twilio", "name": "Twilio", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/twilio", + "documentation": "https://www.home-assistant.io/integrations/twilio", "requirements": [ "twilio==6.19.1" ], diff --git a/homeassistant/components/twilio_call/manifest.json b/homeassistant/components/twilio_call/manifest.json index b235385396be12..3fe8f129b5db60 100644 --- a/homeassistant/components/twilio_call/manifest.json +++ b/homeassistant/components/twilio_call/manifest.json @@ -1,7 +1,7 @@ { "domain": "twilio_call", "name": "Twilio call", - "documentation": "https://www.home-assistant.io/components/twilio_call", + "documentation": "https://www.home-assistant.io/integrations/twilio_call", "requirements": [], "dependencies": [ "twilio" diff --git a/homeassistant/components/twilio_sms/manifest.json b/homeassistant/components/twilio_sms/manifest.json index 2174dc275b52cf..00c843d2dff28e 100644 --- a/homeassistant/components/twilio_sms/manifest.json +++ b/homeassistant/components/twilio_sms/manifest.json @@ -1,7 +1,7 @@ { "domain": "twilio_sms", "name": "Twilio sms", - "documentation": "https://www.home-assistant.io/components/twilio_sms", + "documentation": "https://www.home-assistant.io/integrations/twilio_sms", "requirements": [], "dependencies": [ "twilio" diff --git a/homeassistant/components/twitch/manifest.json b/homeassistant/components/twitch/manifest.json index 80bc795b536d09..f64182f6e4def7 100644 --- a/homeassistant/components/twitch/manifest.json +++ b/homeassistant/components/twitch/manifest.json @@ -1,7 +1,7 @@ { "domain": "twitch", "name": "Twitch", - "documentation": "https://www.home-assistant.io/components/twitch", + "documentation": "https://www.home-assistant.io/integrations/twitch", "requirements": [ "python-twitch-client==0.6.0" ], diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index e721bb669ed41c..2713004343ba5e 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -1,7 +1,7 @@ { "domain": "twitter", "name": "Twitter", - "documentation": "https://www.home-assistant.io/components/twitter", + "documentation": "https://www.home-assistant.io/integrations/twitter", "requirements": [ "TwitterAPI==2.5.9" ], diff --git a/homeassistant/components/ubee/manifest.json b/homeassistant/components/ubee/manifest.json index 39ffe7686579f6..0bf29beb0dc206 100644 --- a/homeassistant/components/ubee/manifest.json +++ b/homeassistant/components/ubee/manifest.json @@ -1,7 +1,7 @@ { "domain": "ubee", "name": "Ubee", - "documentation": "https://www.home-assistant.io/components/ubee", + "documentation": "https://www.home-assistant.io/integrations/ubee", "requirements": [ "pyubee==0.7" ], diff --git a/homeassistant/components/ubus/manifest.json b/homeassistant/components/ubus/manifest.json index f886e84254b179..664ae861442da6 100644 --- a/homeassistant/components/ubus/manifest.json +++ b/homeassistant/components/ubus/manifest.json @@ -1,7 +1,7 @@ { "domain": "ubus", "name": "Ubus", - "documentation": "https://www.home-assistant.io/components/ubus", + "documentation": "https://www.home-assistant.io/integrations/ubus", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ue_smart_radio/manifest.json b/homeassistant/components/ue_smart_radio/manifest.json index 189ac690758551..3711d7cbeb4730 100644 --- a/homeassistant/components/ue_smart_radio/manifest.json +++ b/homeassistant/components/ue_smart_radio/manifest.json @@ -1,7 +1,7 @@ { "domain": "ue_smart_radio", "name": "Ue smart radio", - "documentation": "https://www.home-assistant.io/components/ue_smart_radio", + "documentation": "https://www.home-assistant.io/integrations/ue_smart_radio", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/uk_transport/manifest.json b/homeassistant/components/uk_transport/manifest.json index be44a9d8cc82bb..cf349a205712ad 100644 --- a/homeassistant/components/uk_transport/manifest.json +++ b/homeassistant/components/uk_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "uk_transport", "name": "Uk transport", - "documentation": "https://www.home-assistant.io/components/uk_transport", + "documentation": "https://www.home-assistant.io/integrations/uk_transport", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index d182806c4ac7b7..ecbeb002f04959 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -2,7 +2,7 @@ "domain": "unifi", "name": "Unifi", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/unifi", + "documentation": "https://www.home-assistant.io/integrations/unifi", "requirements": [ "aiounifi==11" ], diff --git a/homeassistant/components/unifi_direct/manifest.json b/homeassistant/components/unifi_direct/manifest.json index 515bd68d011f73..805dc6638bb63a 100644 --- a/homeassistant/components/unifi_direct/manifest.json +++ b/homeassistant/components/unifi_direct/manifest.json @@ -1,7 +1,7 @@ { "domain": "unifi_direct", "name": "Unifi direct", - "documentation": "https://www.home-assistant.io/components/unifi_direct", + "documentation": "https://www.home-assistant.io/integrations/unifi_direct", "requirements": [ "pexpect==4.6.0" ], diff --git a/homeassistant/components/universal/manifest.json b/homeassistant/components/universal/manifest.json index ac72d10f07fbdd..3e066b48598869 100644 --- a/homeassistant/components/universal/manifest.json +++ b/homeassistant/components/universal/manifest.json @@ -1,7 +1,7 @@ { "domain": "universal", "name": "Universal", - "documentation": "https://www.home-assistant.io/components/universal", + "documentation": "https://www.home-assistant.io/integrations/universal", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index efa38286e7e2f6..2cf463d1cf0b25 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -1,7 +1,7 @@ { "domain": "upc_connect", "name": "Upc connect", - "documentation": "https://www.home-assistant.io/components/upc_connect", + "documentation": "https://www.home-assistant.io/integrations/upc_connect", "requirements": ["connect-box==0.2.4"], "dependencies": [], "codeowners": ["@pvizeli"] diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json index 3a58d80f64aafe..62ce608a911f42 100644 --- a/homeassistant/components/upcloud/manifest.json +++ b/homeassistant/components/upcloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "upcloud", "name": "Upcloud", - "documentation": "https://www.home-assistant.io/components/upcloud", + "documentation": "https://www.home-assistant.io/integrations/upcloud", "requirements": [ "upcloud-api==0.4.3" ], diff --git a/homeassistant/components/updater/manifest.json b/homeassistant/components/updater/manifest.json index 9275ef34968249..eb26d6e36b781f 100644 --- a/homeassistant/components/updater/manifest.json +++ b/homeassistant/components/updater/manifest.json @@ -1,7 +1,7 @@ { "domain": "updater", "name": "Updater", - "documentation": "https://www.home-assistant.io/components/updater", + "documentation": "https://www.home-assistant.io/integrations/updater", "requirements": [ "distro==1.4.0" ], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 9aec23a687ce0d..d4446b271f9a18 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -2,7 +2,7 @@ "domain": "upnp", "name": "Upnp", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/upnp", + "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": [ "async-upnp-client==0.14.11" ], diff --git a/homeassistant/components/uptime/manifest.json b/homeassistant/components/uptime/manifest.json index 1019717838108b..5997916e2c3fc8 100644 --- a/homeassistant/components/uptime/manifest.json +++ b/homeassistant/components/uptime/manifest.json @@ -1,7 +1,7 @@ { "domain": "uptime", "name": "Uptime", - "documentation": "https://www.home-assistant.io/components/uptime", + "documentation": "https://www.home-assistant.io/integrations/uptime", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index 375baf12565e00..cc2d1b6c2e85ae 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -1,7 +1,7 @@ { "domain": "uptimerobot", "name": "Uptimerobot", - "documentation": "https://www.home-assistant.io/components/uptimerobot", + "documentation": "https://www.home-assistant.io/integrations/uptimerobot", "requirements": [ "pyuptimerobot==0.0.5" ], diff --git a/homeassistant/components/uscis/manifest.json b/homeassistant/components/uscis/manifest.json index f2ffcfbf8a379d..707b7f278603dd 100644 --- a/homeassistant/components/uscis/manifest.json +++ b/homeassistant/components/uscis/manifest.json @@ -1,7 +1,7 @@ { "domain": "uscis", "name": "Uscis", - "documentation": "https://www.home-assistant.io/components/uscis", + "documentation": "https://www.home-assistant.io/integrations/uscis", "requirements": [ "uscisstatus==0.1.1" ], diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index 0d1c116786ab6b..d1ae97b550ada5 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -1,7 +1,7 @@ { "domain": "usgs_earthquakes_feed", "name": "Usgs earthquakes feed", - "documentation": "https://www.home-assistant.io/components/usgs_earthquakes_feed", + "documentation": "https://www.home-assistant.io/integrations/usgs_earthquakes_feed", "requirements": [ "geojson_client==0.4" ], diff --git a/homeassistant/components/utility_meter/manifest.json b/homeassistant/components/utility_meter/manifest.json index 59f4d1ca21b06b..7a470037ba4f86 100644 --- a/homeassistant/components/utility_meter/manifest.json +++ b/homeassistant/components/utility_meter/manifest.json @@ -1,7 +1,7 @@ { "domain": "utility_meter", "name": "Utility meter", - "documentation": "https://www.home-assistant.io/components/utility_meter", + "documentation": "https://www.home-assistant.io/integrations/utility_meter", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/uvc/manifest.json b/homeassistant/components/uvc/manifest.json index 5c77f9ecc70bab..497bdac656e4cb 100644 --- a/homeassistant/components/uvc/manifest.json +++ b/homeassistant/components/uvc/manifest.json @@ -1,7 +1,7 @@ { "domain": "uvc", "name": "Uvc", - "documentation": "https://www.home-assistant.io/components/uvc", + "documentation": "https://www.home-assistant.io/integrations/uvc", "requirements": [ "uvcclient==0.11.0" ], diff --git a/homeassistant/components/vacuum/manifest.json b/homeassistant/components/vacuum/manifest.json index 8dfbb8ed968c74..69edc40b15be7f 100644 --- a/homeassistant/components/vacuum/manifest.json +++ b/homeassistant/components/vacuum/manifest.json @@ -1,7 +1,7 @@ { "domain": "vacuum", "name": "Vacuum", - "documentation": "https://www.home-assistant.io/components/vacuum", + "documentation": "https://www.home-assistant.io/integrations/vacuum", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 4f9b0f4d1269bd..51ecda914045de 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -1,7 +1,7 @@ { "domain": "vallox", "name": "Vallox", - "documentation": "https://www.home-assistant.io/components/vallox", + "documentation": "https://www.home-assistant.io/integrations/vallox", "requirements": [ "vallox-websocket-api==2.2.0" ], diff --git a/homeassistant/components/vasttrafik/manifest.json b/homeassistant/components/vasttrafik/manifest.json index 47153dcf17f50d..c4e3a2c97bb431 100644 --- a/homeassistant/components/vasttrafik/manifest.json +++ b/homeassistant/components/vasttrafik/manifest.json @@ -1,7 +1,7 @@ { "domain": "vasttrafik", "name": "Vasttrafik", - "documentation": "https://www.home-assistant.io/components/vasttrafik", + "documentation": "https://www.home-assistant.io/integrations/vasttrafik", "requirements": [ "vtjp==0.1.14" ], diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index b071b354d744a4..1d9401f6cfe827 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -1,7 +1,7 @@ { "domain": "velbus", "name": "Velbus", - "documentation": "https://www.home-assistant.io/components/velbus", + "documentation": "https://www.home-assistant.io/integrations/velbus", "requirements": [ "python-velbus==2.0.27" ], diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index 9f1f4a7200afcb..783e23a8171800 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -1,7 +1,7 @@ { "domain": "velux", "name": "Velux", - "documentation": "https://www.home-assistant.io/components/velux", + "documentation": "https://www.home-assistant.io/integrations/velux", "requirements": [ "pyvlx==0.2.11" ], diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 655ba019e8c93a..e8e36d04467ce9 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -1,7 +1,7 @@ { "domain": "venstar", "name": "Venstar", - "documentation": "https://www.home-assistant.io/components/venstar", + "documentation": "https://www.home-assistant.io/integrations/venstar", "requirements": [ "venstarcolortouch==0.9" ], diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index b2f1581e76f191..120ec241d60f7b 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -1,7 +1,7 @@ { "domain": "vera", "name": "Vera", - "documentation": "https://www.home-assistant.io/components/vera", + "documentation": "https://www.home-assistant.io/integrations/vera", "requirements": [ "pyvera==0.3.6" ], diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 7c895233f770d0..38ea8c7314769e 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -1,7 +1,7 @@ { "domain": "verisure", "name": "Verisure", - "documentation": "https://www.home-assistant.io/components/verisure", + "documentation": "https://www.home-assistant.io/integrations/verisure", "requirements": [ "jsonpath==0.75", "vsure==1.5.2" diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 815e7ff9a25794..3f35b0dc9a56ae 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -1,7 +1,7 @@ { "domain": "version", "name": "Version", - "documentation": "https://www.home-assistant.io/components/version", + "documentation": "https://www.home-assistant.io/integrations/version", "requirements": [ "pyhaversion==3.1.0" ], diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index 53cc96be388c55..d52380c10253f7 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -1,7 +1,7 @@ { "domain": "vesync", "name": "VeSync", - "documentation": "https://www.home-assistant.io/components/vesync", + "documentation": "https://www.home-assistant.io/integrations/vesync", "dependencies": [], "codeowners": ["@markperdue", "@webdjoe"], "requirements": ["pyvesync==1.1.0"], diff --git a/homeassistant/components/viaggiatreno/manifest.json b/homeassistant/components/viaggiatreno/manifest.json index e145b26b0c9a42..99857fc2f7ebf3 100644 --- a/homeassistant/components/viaggiatreno/manifest.json +++ b/homeassistant/components/viaggiatreno/manifest.json @@ -1,7 +1,7 @@ { "domain": "viaggiatreno", "name": "Viaggiatreno", - "documentation": "https://www.home-assistant.io/components/viaggiatreno", + "documentation": "https://www.home-assistant.io/integrations/viaggiatreno", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index e5f55b20ddaf8d..9f7c703fe4b514 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -1,7 +1,7 @@ { "domain": "vicare", "name": "Viessmann ViCare", - "documentation": "https://www.home-assistant.io/components/vicare", + "documentation": "https://www.home-assistant.io/integrations/vicare", "dependencies": [], "codeowners": ["@oischinger"], "requirements": ["PyViCare==0.1.1"] diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index cce2307bc4b554..20b2ac347f685e 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -1,7 +1,7 @@ { "domain": "vivotek", "name": "Vivotek", - "documentation": "https://www.home-assistant.io/components/vivotek", + "documentation": "https://www.home-assistant.io/integrations/vivotek", "requirements": [ "libpyvivotek==0.2.2" ], diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index c65204d78e8c71..682405a375b869 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -1,7 +1,7 @@ { "domain": "vizio", "name": "Vizio", - "documentation": "https://www.home-assistant.io/components/vizio", + "documentation": "https://www.home-assistant.io/integrations/vizio", "requirements": [ "pyvizio==0.0.7" ], diff --git a/homeassistant/components/vlc/manifest.json b/homeassistant/components/vlc/manifest.json index a40b0e8c7d61d1..0dfc9f1ceb9a01 100644 --- a/homeassistant/components/vlc/manifest.json +++ b/homeassistant/components/vlc/manifest.json @@ -1,7 +1,7 @@ { "domain": "vlc", "name": "Vlc", - "documentation": "https://www.home-assistant.io/components/vlc", + "documentation": "https://www.home-assistant.io/integrations/vlc", "requirements": [ "python-vlc==1.1.2" ], diff --git a/homeassistant/components/vlc_telnet/manifest.json b/homeassistant/components/vlc_telnet/manifest.json index 1e0f1c71df5061..7894eb2982b4cb 100644 --- a/homeassistant/components/vlc_telnet/manifest.json +++ b/homeassistant/components/vlc_telnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "vlc_telnet", "name": "VLC telnet", - "documentation": "https://www.home-assistant.io/components/vlc-telnet", + "documentation": "https://www.home-assistant.io/integrations/vlc-telnet", "requirements": [ "python-telnet-vlc==1.0.4" ], diff --git a/homeassistant/components/voicerss/manifest.json b/homeassistant/components/voicerss/manifest.json index 6f0b4ae5fd258d..7a079d9ccf2b82 100644 --- a/homeassistant/components/voicerss/manifest.json +++ b/homeassistant/components/voicerss/manifest.json @@ -1,7 +1,7 @@ { "domain": "voicerss", "name": "Voicerss", - "documentation": "https://www.home-assistant.io/components/voicerss", + "documentation": "https://www.home-assistant.io/integrations/voicerss", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index db068e350566d9..0b210bc780b2d8 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -1,7 +1,7 @@ { "domain": "volkszaehler", "name": "Volkszaehler", - "documentation": "https://www.home-assistant.io/components/volkszaehler", + "documentation": "https://www.home-assistant.io/integrations/volkszaehler", "requirements": [ "volkszaehler==0.1.2" ], diff --git a/homeassistant/components/volumio/manifest.json b/homeassistant/components/volumio/manifest.json index e7c4bac4abd71b..a97c9d637ef01c 100644 --- a/homeassistant/components/volumio/manifest.json +++ b/homeassistant/components/volumio/manifest.json @@ -1,7 +1,7 @@ { "domain": "volumio", "name": "Volumio", - "documentation": "https://www.home-assistant.io/components/volumio", + "documentation": "https://www.home-assistant.io/integrations/volumio", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index aa691d7766c75e..3f75c391115fd8 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -1,7 +1,7 @@ { "domain": "volvooncall", "name": "Volvooncall", - "documentation": "https://www.home-assistant.io/components/volvooncall", + "documentation": "https://www.home-assistant.io/integrations/volvooncall", "requirements": [ "volvooncall==0.8.7" ], diff --git a/homeassistant/components/vultr/manifest.json b/homeassistant/components/vultr/manifest.json index 5f5461f2d63fb7..0a0afe3d71b59b 100644 --- a/homeassistant/components/vultr/manifest.json +++ b/homeassistant/components/vultr/manifest.json @@ -1,7 +1,7 @@ { "domain": "vultr", "name": "Vultr", - "documentation": "https://www.home-assistant.io/components/vultr", + "documentation": "https://www.home-assistant.io/integrations/vultr", "requirements": [ "vultr==0.1.2" ], diff --git a/homeassistant/components/w800rf32/manifest.json b/homeassistant/components/w800rf32/manifest.json index 920ee1120a7c50..89b0ac591ea937 100644 --- a/homeassistant/components/w800rf32/manifest.json +++ b/homeassistant/components/w800rf32/manifest.json @@ -1,7 +1,7 @@ { "domain": "w800rf32", "name": "W800rf32", - "documentation": "https://www.home-assistant.io/components/w800rf32", + "documentation": "https://www.home-assistant.io/integrations/w800rf32", "requirements": [ "pyW800rf32==0.1" ], diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index dc689f8d617f56..ef6dbd0647083c 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -1,7 +1,7 @@ { "domain": "wake_on_lan", "name": "Wake on lan", - "documentation": "https://www.home-assistant.io/components/wake_on_lan", + "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", "requirements": [ "wakeonlan==1.1.6" ], diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index 4b692c669d1ea7..8ce03e2e8e2608 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -1,7 +1,7 @@ { "domain": "waqi", "name": "Waqi", - "documentation": "https://www.home-assistant.io/components/waqi", + "documentation": "https://www.home-assistant.io/integrations/waqi", "requirements": [ "waqiasync==1.0.0" ], diff --git a/homeassistant/components/water_heater/manifest.json b/homeassistant/components/water_heater/manifest.json index e291777483ef00..7ed6493b843a89 100644 --- a/homeassistant/components/water_heater/manifest.json +++ b/homeassistant/components/water_heater/manifest.json @@ -1,7 +1,7 @@ { "domain": "water_heater", "name": "Water heater", - "documentation": "https://www.home-assistant.io/components/water_heater", + "documentation": "https://www.home-assistant.io/integrations/water_heater", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/waterfurnace/manifest.json b/homeassistant/components/waterfurnace/manifest.json index 57aa663a348ebb..e2982bd0145893 100644 --- a/homeassistant/components/waterfurnace/manifest.json +++ b/homeassistant/components/waterfurnace/manifest.json @@ -1,7 +1,7 @@ { "domain": "waterfurnace", "name": "Waterfurnace", - "documentation": "https://www.home-assistant.io/components/waterfurnace", + "documentation": "https://www.home-assistant.io/integrations/waterfurnace", "requirements": [ "waterfurnace==1.1.0" ], diff --git a/homeassistant/components/watson_iot/manifest.json b/homeassistant/components/watson_iot/manifest.json index 8896f34f976afe..20834bf0bf4ec5 100644 --- a/homeassistant/components/watson_iot/manifest.json +++ b/homeassistant/components/watson_iot/manifest.json @@ -1,7 +1,7 @@ { "domain": "watson_iot", "name": "Watson iot", - "documentation": "https://www.home-assistant.io/components/watson_iot", + "documentation": "https://www.home-assistant.io/integrations/watson_iot", "requirements": [ "ibmiotf==0.3.4" ], diff --git a/homeassistant/components/watson_tts/manifest.json b/homeassistant/components/watson_tts/manifest.json index d40baaca13220e..4cde3f764e5080 100644 --- a/homeassistant/components/watson_tts/manifest.json +++ b/homeassistant/components/watson_tts/manifest.json @@ -1,7 +1,7 @@ { "domain": "watson_tts", "name": "IBM Watson TTS", - "documentation": "https://www.home-assistant.io/components/watson_tts", + "documentation": "https://www.home-assistant.io/integrations/watson_tts", "requirements": [ "ibm-watson==3.0.3" ], diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 09ae4f812d7ade..85bcc19032e965 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -1,7 +1,7 @@ { "domain": "waze_travel_time", "name": "Waze travel time", - "documentation": "https://www.home-assistant.io/components/waze_travel_time", + "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "requirements": [ "WazeRouteCalculator==0.10" ], diff --git a/homeassistant/components/weather/manifest.json b/homeassistant/components/weather/manifest.json index 7008c033f95bc9..568ce57ed6213b 100644 --- a/homeassistant/components/weather/manifest.json +++ b/homeassistant/components/weather/manifest.json @@ -1,7 +1,7 @@ { "domain": "weather", "name": "Weather", - "documentation": "https://www.home-assistant.io/components/weather", + "documentation": "https://www.home-assistant.io/integrations/weather", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/webhook/manifest.json b/homeassistant/components/webhook/manifest.json index 384e61aed2a848..f93d8a5e295234 100644 --- a/homeassistant/components/webhook/manifest.json +++ b/homeassistant/components/webhook/manifest.json @@ -1,7 +1,7 @@ { "domain": "webhook", "name": "Webhook", - "documentation": "https://www.home-assistant.io/components/webhook", + "documentation": "https://www.home-assistant.io/integrations/webhook", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/weblink/manifest.json b/homeassistant/components/weblink/manifest.json index 7c30ad6c5d3549..7cdf8bea4ff1c4 100644 --- a/homeassistant/components/weblink/manifest.json +++ b/homeassistant/components/weblink/manifest.json @@ -1,7 +1,7 @@ { "domain": "weblink", "name": "Weblink", - "documentation": "https://www.home-assistant.io/components/weblink", + "documentation": "https://www.home-assistant.io/integrations/weblink", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 4dd2f92628d630..dcf908cd6037d4 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -1,7 +1,7 @@ { "domain": "webostv", "name": "Webostv", - "documentation": "https://www.home-assistant.io/components/webostv", + "documentation": "https://www.home-assistant.io/integrations/webostv", "requirements": [ "pylgtv==0.1.9", "websockets==6.0" diff --git a/homeassistant/components/websocket_api/manifest.json b/homeassistant/components/websocket_api/manifest.json index bc630b2947fca5..9826b11ec45e36 100644 --- a/homeassistant/components/websocket_api/manifest.json +++ b/homeassistant/components/websocket_api/manifest.json @@ -1,7 +1,7 @@ { "domain": "websocket_api", "name": "Websocket api", - "documentation": "https://www.home-assistant.io/components/websocket_api", + "documentation": "https://www.home-assistant.io/integrations/websocket_api", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 1902df1060b317..aa863bcff0d4ad 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -2,7 +2,7 @@ "domain": "wemo", "name": "Wemo", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/wemo", + "documentation": "https://www.home-assistant.io/integrations/wemo", "requirements": [ "pywemo==0.4.34" ], diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json index 6040c8655b9f25..1566366362a44b 100644 --- a/homeassistant/components/whois/manifest.json +++ b/homeassistant/components/whois/manifest.json @@ -1,7 +1,7 @@ { "domain": "whois", "name": "Whois", - "documentation": "https://www.home-assistant.io/components/whois", + "documentation": "https://www.home-assistant.io/integrations/whois", "requirements": [ "python-whois==0.7.2" ], diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index cddfdc5dc9c878..acf9c38e6322c1 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -1,7 +1,7 @@ { "domain": "wink", "name": "Wink", - "documentation": "https://www.home-assistant.io/components/wink", + "documentation": "https://www.home-assistant.io/integrations/wink", "requirements": [ "pubnubsub-handler==1.0.8", "python-wink==1.10.5" diff --git a/homeassistant/components/wirelesstag/manifest.json b/homeassistant/components/wirelesstag/manifest.json index c3da00ce951c75..7472898b7ca6f2 100644 --- a/homeassistant/components/wirelesstag/manifest.json +++ b/homeassistant/components/wirelesstag/manifest.json @@ -1,7 +1,7 @@ { "domain": "wirelesstag", "name": "Wirelesstag", - "documentation": "https://www.home-assistant.io/components/wirelesstag", + "documentation": "https://www.home-assistant.io/integrations/wirelesstag", "requirements": [ "wirelesstagpy==0.4.0" ], diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 726d9f13eda0b0..d38b69f2248bcc 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -2,7 +2,7 @@ "domain": "withings", "name": "Withings", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/withings", + "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ "nokia==1.2.0" ], diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index ba8712f0575282..4b407e9523540c 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -1,7 +1,7 @@ { "domain": "workday", "name": "Workday", - "documentation": "https://www.home-assistant.io/components/workday", + "documentation": "https://www.home-assistant.io/integrations/workday", "requirements": [ "holidays==0.9.11" ], diff --git a/homeassistant/components/worldclock/manifest.json b/homeassistant/components/worldclock/manifest.json index 2da33f942b8f65..8f7b72491b8eb7 100644 --- a/homeassistant/components/worldclock/manifest.json +++ b/homeassistant/components/worldclock/manifest.json @@ -1,7 +1,7 @@ { "domain": "worldclock", "name": "Worldclock", - "documentation": "https://www.home-assistant.io/components/worldclock", + "documentation": "https://www.home-assistant.io/integrations/worldclock", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/worldtidesinfo/manifest.json b/homeassistant/components/worldtidesinfo/manifest.json index dfc116c97db359..467a98a6660e22 100644 --- a/homeassistant/components/worldtidesinfo/manifest.json +++ b/homeassistant/components/worldtidesinfo/manifest.json @@ -1,7 +1,7 @@ { "domain": "worldtidesinfo", "name": "Worldtidesinfo", - "documentation": "https://www.home-assistant.io/components/worldtidesinfo", + "documentation": "https://www.home-assistant.io/integrations/worldtidesinfo", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/worxlandroid/manifest.json b/homeassistant/components/worxlandroid/manifest.json index 3e7c626ddd0f8a..b112bb7771cde5 100644 --- a/homeassistant/components/worxlandroid/manifest.json +++ b/homeassistant/components/worxlandroid/manifest.json @@ -1,7 +1,7 @@ { "domain": "worxlandroid", "name": "Worxlandroid", - "documentation": "https://www.home-assistant.io/components/worxlandroid", + "documentation": "https://www.home-assistant.io/integrations/worxlandroid", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/wsdot/manifest.json b/homeassistant/components/wsdot/manifest.json index c778ed1049f598..1c20b8842238e0 100644 --- a/homeassistant/components/wsdot/manifest.json +++ b/homeassistant/components/wsdot/manifest.json @@ -1,7 +1,7 @@ { "domain": "wsdot", "name": "Wsdot", - "documentation": "https://www.home-assistant.io/components/wsdot", + "documentation": "https://www.home-assistant.io/integrations/wsdot", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/wunderground/manifest.json b/homeassistant/components/wunderground/manifest.json index d14c9db419a53a..8309f03bca4be0 100644 --- a/homeassistant/components/wunderground/manifest.json +++ b/homeassistant/components/wunderground/manifest.json @@ -1,7 +1,7 @@ { "domain": "wunderground", "name": "Wunderground", - "documentation": "https://www.home-assistant.io/components/wunderground", + "documentation": "https://www.home-assistant.io/integrations/wunderground", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/wunderlist/manifest.json b/homeassistant/components/wunderlist/manifest.json index 505447f454c031..90a55ad48e8f67 100644 --- a/homeassistant/components/wunderlist/manifest.json +++ b/homeassistant/components/wunderlist/manifest.json @@ -1,7 +1,7 @@ { "domain": "wunderlist", "name": "Wunderlist", - "documentation": "https://www.home-assistant.io/components/wunderlist", + "documentation": "https://www.home-assistant.io/integrations/wunderlist", "requirements": [ "wunderpy2==0.1.6" ], diff --git a/homeassistant/components/wwlln/manifest.json b/homeassistant/components/wwlln/manifest.json index 189b9365105159..a7bf14454da8d5 100644 --- a/homeassistant/components/wwlln/manifest.json +++ b/homeassistant/components/wwlln/manifest.json @@ -2,7 +2,7 @@ "domain": "wwlln", "name": "World Wide Lightning Location Network", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/wwlln", + "documentation": "https://www.home-assistant.io/integrations/wwlln", "requirements": [ "aiowwlln==2.0.2" ], diff --git a/homeassistant/components/x10/manifest.json b/homeassistant/components/x10/manifest.json index 2fbe16a6e7adae..bae5247ffbc553 100644 --- a/homeassistant/components/x10/manifest.json +++ b/homeassistant/components/x10/manifest.json @@ -1,7 +1,7 @@ { "domain": "x10", "name": "X10", - "documentation": "https://www.home-assistant.io/components/x10", + "documentation": "https://www.home-assistant.io/integrations/x10", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/xbox_live/manifest.json b/homeassistant/components/xbox_live/manifest.json index 5baf928352d514..79f4ce6c87fc29 100644 --- a/homeassistant/components/xbox_live/manifest.json +++ b/homeassistant/components/xbox_live/manifest.json @@ -1,7 +1,7 @@ { "domain": "xbox_live", "name": "Xbox live", - "documentation": "https://www.home-assistant.io/components/xbox_live", + "documentation": "https://www.home-assistant.io/integrations/xbox_live", "requirements": [ "xboxapi==0.1.1" ], diff --git a/homeassistant/components/xeoma/manifest.json b/homeassistant/components/xeoma/manifest.json index ee8ed2f6de31de..7cf061018b9615 100644 --- a/homeassistant/components/xeoma/manifest.json +++ b/homeassistant/components/xeoma/manifest.json @@ -1,7 +1,7 @@ { "domain": "xeoma", "name": "Xeoma", - "documentation": "https://www.home-assistant.io/components/xeoma", + "documentation": "https://www.home-assistant.io/integrations/xeoma", "requirements": [ "pyxeoma==1.4.1" ], diff --git a/homeassistant/components/xfinity/manifest.json b/homeassistant/components/xfinity/manifest.json index 71750ccf0889a4..9e800dc2e4a341 100644 --- a/homeassistant/components/xfinity/manifest.json +++ b/homeassistant/components/xfinity/manifest.json @@ -1,7 +1,7 @@ { "domain": "xfinity", "name": "Xfinity", - "documentation": "https://www.home-assistant.io/components/xfinity", + "documentation": "https://www.home-assistant.io/integrations/xfinity", "requirements": [ "xfinity-gateway==0.0.4" ], diff --git a/homeassistant/components/xiaomi/manifest.json b/homeassistant/components/xiaomi/manifest.json index d3587100501f04..a607cda511ebae 100644 --- a/homeassistant/components/xiaomi/manifest.json +++ b/homeassistant/components/xiaomi/manifest.json @@ -1,7 +1,7 @@ { "domain": "xiaomi", "name": "Xiaomi", - "documentation": "https://www.home-assistant.io/components/xiaomi", + "documentation": "https://www.home-assistant.io/integrations/xiaomi", "requirements": [], "dependencies": [ "ffmpeg" diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index 36da259f82e5cf..9eeddd357f6bf6 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -1,7 +1,7 @@ { "domain": "xiaomi_aqara", "name": "Xiaomi aqara", - "documentation": "https://www.home-assistant.io/components/xiaomi_aqara", + "documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara", "requirements": [ "PyXiaomiGateway==0.12.4" ], diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index d7e0d0d732eee8..4c01cce2d3c42e 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -1,7 +1,7 @@ { "domain": "xiaomi_miio", "name": "Xiaomi miio", - "documentation": "https://www.home-assistant.io/components/xiaomi_miio", + "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "requirements": [ "construct==2.9.45", "python-miio==0.4.5" diff --git a/homeassistant/components/xiaomi_tv/manifest.json b/homeassistant/components/xiaomi_tv/manifest.json index 26940a57c78743..740eaf3ea1caed 100644 --- a/homeassistant/components/xiaomi_tv/manifest.json +++ b/homeassistant/components/xiaomi_tv/manifest.json @@ -1,7 +1,7 @@ { "domain": "xiaomi_tv", "name": "Xiaomi tv", - "documentation": "https://www.home-assistant.io/components/xiaomi_tv", + "documentation": "https://www.home-assistant.io/integrations/xiaomi_tv", "requirements": [ "pymitv==1.4.3" ], diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 3d2c3a5e9119cb..21255497503b47 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -1,7 +1,7 @@ { "domain": "xmpp", "name": "Xmpp", - "documentation": "https://www.home-assistant.io/components/xmpp", + "documentation": "https://www.home-assistant.io/integrations/xmpp", "requirements": [ "slixmpp==1.4.2" ], diff --git a/homeassistant/components/xs1/manifest.json b/homeassistant/components/xs1/manifest.json index 4ee13acf6472e7..290c552309b8ab 100644 --- a/homeassistant/components/xs1/manifest.json +++ b/homeassistant/components/xs1/manifest.json @@ -1,7 +1,7 @@ { "domain": "xs1", "name": "Xs1", - "documentation": "https://www.home-assistant.io/components/xs1", + "documentation": "https://www.home-assistant.io/integrations/xs1", "requirements": [ "xs1-api-client==2.3.5" ], diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index 7b786c7bf7c58e..05e979ffb0a065 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -1,7 +1,7 @@ { "domain": "yale_smart_alarm", "name": "Yale smart alarm", - "documentation": "https://www.home-assistant.io/components/yale_smart_alarm", + "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", "requirements": [ "yalesmartalarmclient==0.1.6" ], diff --git a/homeassistant/components/yamaha/manifest.json b/homeassistant/components/yamaha/manifest.json index 5a277fc7ce879c..bacb9fc33055ed 100644 --- a/homeassistant/components/yamaha/manifest.json +++ b/homeassistant/components/yamaha/manifest.json @@ -1,7 +1,7 @@ { "domain": "yamaha", "name": "Yamaha", - "documentation": "https://www.home-assistant.io/components/yamaha", + "documentation": "https://www.home-assistant.io/integrations/yamaha", "requirements": [ "rxv==0.6.0" ], diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 7769026e09279a..ea36c4921c56c2 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -1,7 +1,7 @@ { "domain": "yamaha_musiccast", "name": "Yamaha musiccast", - "documentation": "https://www.home-assistant.io/components/yamaha_musiccast", + "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", "requirements": [ "pymusiccast==0.1.6" ], diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index 6c633f848c0c68..91267b43480b06 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "yandex_transport", "name": "Yandex Transport", - "documentation": "https://www.home-assistant.io/components/yandex_transport", + "documentation": "https://www.home-assistant.io/integrations/yandex_transport", "requirements": [ "ya_ma==0.3.7" ], diff --git a/homeassistant/components/yandextts/manifest.json b/homeassistant/components/yandextts/manifest.json index 7f622a1e25fe68..66a546abdb41e5 100644 --- a/homeassistant/components/yandextts/manifest.json +++ b/homeassistant/components/yandextts/manifest.json @@ -1,7 +1,7 @@ { "domain": "yandextts", "name": "Yandextts", - "documentation": "https://www.home-assistant.io/components/yandextts", + "documentation": "https://www.home-assistant.io/integrations/yandextts", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 061d2b065c4cbd..3d27d5bd393add 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -1,7 +1,7 @@ { "domain": "yeelight", "name": "Yeelight", - "documentation": "https://www.home-assistant.io/components/yeelight", + "documentation": "https://www.home-assistant.io/integrations/yeelight", "requirements": [ "yeelight==0.5.0" ], diff --git a/homeassistant/components/yeelightsunflower/manifest.json b/homeassistant/components/yeelightsunflower/manifest.json index 1a75472b80131a..390ff7427244e4 100644 --- a/homeassistant/components/yeelightsunflower/manifest.json +++ b/homeassistant/components/yeelightsunflower/manifest.json @@ -1,7 +1,7 @@ { "domain": "yeelightsunflower", "name": "Yeelightsunflower", - "documentation": "https://www.home-assistant.io/components/yeelightsunflower", + "documentation": "https://www.home-assistant.io/integrations/yeelightsunflower", "requirements": [ "yeelightsunflower==0.0.10" ], diff --git a/homeassistant/components/yessssms/manifest.json b/homeassistant/components/yessssms/manifest.json index c7b5535d03c6f6..b68649525c2a04 100644 --- a/homeassistant/components/yessssms/manifest.json +++ b/homeassistant/components/yessssms/manifest.json @@ -1,7 +1,7 @@ { "domain": "yessssms", "name": "Yessssms", - "documentation": "https://www.home-assistant.io/components/yessssms", + "documentation": "https://www.home-assistant.io/integrations/yessssms", "requirements": [ "YesssSMS==0.4.1" ], diff --git a/homeassistant/components/yi/manifest.json b/homeassistant/components/yi/manifest.json index bb7fbf55cbc205..461f3e24330113 100644 --- a/homeassistant/components/yi/manifest.json +++ b/homeassistant/components/yi/manifest.json @@ -1,7 +1,7 @@ { "domain": "yi", "name": "Yi", - "documentation": "https://www.home-assistant.io/components/yi", + "documentation": "https://www.home-assistant.io/integrations/yi", "requirements": [ "aioftp==0.12.0" ], diff --git a/homeassistant/components/yr/manifest.json b/homeassistant/components/yr/manifest.json index 7f06ddddcb5700..d49004cc0e3944 100644 --- a/homeassistant/components/yr/manifest.json +++ b/homeassistant/components/yr/manifest.json @@ -1,7 +1,7 @@ { "domain": "yr", "name": "Yr", - "documentation": "https://www.home-assistant.io/components/yr", + "documentation": "https://www.home-assistant.io/integrations/yr", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/yweather/manifest.json b/homeassistant/components/yweather/manifest.json index c3048601595f85..482951e156f6ec 100644 --- a/homeassistant/components/yweather/manifest.json +++ b/homeassistant/components/yweather/manifest.json @@ -1,7 +1,7 @@ { "domain": "yweather", "name": "Yweather", - "documentation": "https://www.home-assistant.io/components/yweather", + "documentation": "https://www.home-assistant.io/integrations/yweather", "requirements": [ "yahooweather==0.10" ], diff --git a/homeassistant/components/zabbix/manifest.json b/homeassistant/components/zabbix/manifest.json index c0f100fa62ffa7..5959fba5fa2b17 100644 --- a/homeassistant/components/zabbix/manifest.json +++ b/homeassistant/components/zabbix/manifest.json @@ -1,7 +1,7 @@ { "domain": "zabbix", "name": "Zabbix", - "documentation": "https://www.home-assistant.io/components/zabbix", + "documentation": "https://www.home-assistant.io/integrations/zabbix", "requirements": [ "pyzabbix==0.7.4" ], diff --git a/homeassistant/components/zamg/manifest.json b/homeassistant/components/zamg/manifest.json index ce16e1b523c392..2b95a9ec0c7d9d 100644 --- a/homeassistant/components/zamg/manifest.json +++ b/homeassistant/components/zamg/manifest.json @@ -1,7 +1,7 @@ { "domain": "zamg", "name": "Zamg", - "documentation": "https://www.home-assistant.io/components/zamg", + "documentation": "https://www.home-assistant.io/integrations/zamg", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/zengge/manifest.json b/homeassistant/components/zengge/manifest.json index b846c95f5fa57c..e24e4537837948 100644 --- a/homeassistant/components/zengge/manifest.json +++ b/homeassistant/components/zengge/manifest.json @@ -1,7 +1,7 @@ { "domain": "zengge", "name": "Zengge", - "documentation": "https://www.home-assistant.io/components/zengge", + "documentation": "https://www.home-assistant.io/integrations/zengge", "requirements": [ "zengge==0.2" ], diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 1461a54d147a7c..39f016e9d0e8ca 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -1,7 +1,7 @@ { "domain": "zeroconf", "name": "Zeroconf", - "documentation": "https://www.home-assistant.io/components/zeroconf", + "documentation": "https://www.home-assistant.io/integrations/zeroconf", "requirements": [ "zeroconf==0.23.0" ], diff --git a/homeassistant/components/zestimate/manifest.json b/homeassistant/components/zestimate/manifest.json index 4d1a55eaa09596..79c89406fac4dc 100644 --- a/homeassistant/components/zestimate/manifest.json +++ b/homeassistant/components/zestimate/manifest.json @@ -1,7 +1,7 @@ { "domain": "zestimate", "name": "Zestimate", - "documentation": "https://www.home-assistant.io/components/zestimate", + "documentation": "https://www.home-assistant.io/integrations/zestimate", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index f0f6389a061e13..ab8a20822a1365 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -2,7 +2,7 @@ "domain": "zha", "name": "Zigbee Home Automation", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/zha", + "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.10.0", "zha-quirks==0.0.26", diff --git a/homeassistant/components/zhong_hong/manifest.json b/homeassistant/components/zhong_hong/manifest.json index 6382a830dcfdb4..32ee69643216b1 100644 --- a/homeassistant/components/zhong_hong/manifest.json +++ b/homeassistant/components/zhong_hong/manifest.json @@ -1,7 +1,7 @@ { "domain": "zhong_hong", "name": "Zhong hong", - "documentation": "https://www.home-assistant.io/components/zhong_hong", + "documentation": "https://www.home-assistant.io/integrations/zhong_hong", "requirements": [ "zhong_hong_hvac==1.0.9" ], diff --git a/homeassistant/components/zigbee/manifest.json b/homeassistant/components/zigbee/manifest.json index 1e4076b84392c5..3968b0e294f624 100644 --- a/homeassistant/components/zigbee/manifest.json +++ b/homeassistant/components/zigbee/manifest.json @@ -1,7 +1,7 @@ { "domain": "zigbee", "name": "Zigbee", - "documentation": "https://www.home-assistant.io/components/zigbee", + "documentation": "https://www.home-assistant.io/integrations/zigbee", "requirements": [ "xbee-helper==0.0.7" ], diff --git a/homeassistant/components/ziggo_mediabox_xl/manifest.json b/homeassistant/components/ziggo_mediabox_xl/manifest.json index 9e587137922e70..ff9e64ae78e5c2 100644 --- a/homeassistant/components/ziggo_mediabox_xl/manifest.json +++ b/homeassistant/components/ziggo_mediabox_xl/manifest.json @@ -1,7 +1,7 @@ { "domain": "ziggo_mediabox_xl", "name": "Ziggo mediabox xl", - "documentation": "https://www.home-assistant.io/components/ziggo_mediabox_xl", + "documentation": "https://www.home-assistant.io/integrations/ziggo_mediabox_xl", "requirements": [ "ziggo-mediabox-xl==1.1.0" ], diff --git a/homeassistant/components/zone/manifest.json b/homeassistant/components/zone/manifest.json index e9281fec3f785e..7a8cdcf6c6c0dc 100644 --- a/homeassistant/components/zone/manifest.json +++ b/homeassistant/components/zone/manifest.json @@ -2,7 +2,7 @@ "domain": "zone", "name": "Zone", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/zone", + "documentation": "https://www.home-assistant.io/integrations/zone", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index 9d371fbabf7670..c29a97e857eca6 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -1,7 +1,7 @@ { "domain": "zoneminder", "name": "Zoneminder", - "documentation": "https://www.home-assistant.io/components/zoneminder", + "documentation": "https://www.home-assistant.io/integrations/zoneminder", "requirements": [ "zm-py==0.3.3" ], diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index f88945fa281279..9268a50a14d78c 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -2,7 +2,7 @@ "domain": "zwave", "name": "Z-Wave", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/zwave", + "documentation": "https://www.home-assistant.io/integrations/zwave", "requirements": [ "homeassistant-pyozw==0.1.4", "pydispatcher==2.0.5" From c78b3a44391a79f65522a03c7dddafe785a4922d Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 2 Oct 2019 17:27:13 +0100 Subject: [PATCH 250/296] Tweak geniushub and bump client to v0.6.26 (#26640) * use state attribute rather than type * HA style tweaks * small tweak * bump client * add more device_state_attributes * bump client * small tweak * bump client for concurrent IO * force snake_case, and refactor (consolidate) Devices/Zones * force snake_case, and refactor (consolidate) Devices/Zones 2 * force snake_case, and refactor (consolidate) Devices/Zones 3 * refactor last_comms / wakeup_interval check * movement sensor is dynamic, and tweaking * tweak * bump client to v0.6.20 * dummy * dummy 2 * bump client to handle another edge case * use entity_id fro zones * small tweak * bump client to 0.6.22 * add recursive snake_case converter * fix regression * fix regression 2 * fix regression 3 * remove Awaitables * don't dynamically create function every scan_interval * log kast_comms as localtime, delint dt_util * add sensors fro v1 API * tweak entity_id * bump client * bump client to v0.6.24 * bump client to v0.6.25 * explicit device attrs, dt as UTC * add unique_id, remove entity_id * Bump client to 0.6.26 - add Hub UID * remove convert_dict() * add mac_address (uid) for v1 API * tweak var names * add UID.upper() to avoid unwanted unique_id changes * Update homeassistant/components/geniushub/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/geniushub/__init__.py Co-Authored-By: Martin Hjelmare * remove underscores * refactor for broker * ready now * validate UID (MAC address) * move uid to broker * use existing constant * pass client to broker --- .../components/geniushub/__init__.py | 198 +++++++++++++++--- .../components/geniushub/binary_sensor.py | 49 ++--- homeassistant/components/geniushub/climate.py | 104 +++------ .../components/geniushub/manifest.json | 2 +- homeassistant/components/geniushub/sensor.py | 78 ++++--- .../components/geniushub/water_heater.py | 107 +++------- requirements_all.txt | 2 +- 7 files changed, 284 insertions(+), 256 deletions(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 45f3f91cd6d82a..d9f6c877cbcb39 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -1,14 +1,22 @@ """Support for a Genius Hub system.""" from datetime import timedelta import logging -from typing import Awaitable +from typing import Any, Dict, Optional import aiohttp import voluptuous as vol from geniushubclient import GeniusHub -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_HOST, + CONF_MAC, + CONF_PASSWORD, + CONF_TOKEN, + CONF_USERNAME, + TEMP_CELSIUS, +) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -19,39 +27,66 @@ ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +import homeassistant.util.dt as dt_util + +ATTR_DURATION = "duration" _LOGGER = logging.getLogger(__name__) DOMAIN = "geniushub" +# temperature is repeated here, as it gives access to high-precision temps +GH_ZONE_ATTRS = ["mode", "temperature", "type", "occupied", "override"] +GH_DEVICE_ATTRS = { + "luminance": "luminance", + "measuredTemperature": "measured_temperature", + "occupancyTrigger": "occupancy_trigger", + "setback": "setback", + "setTemperature": "set_temperature", + "wakeupInterval": "wakeup_interval", +} + SCAN_INTERVAL = timedelta(seconds=60) -_V1_API_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): cv.string}) -_V3_API_SCHEMA = vol.Schema( +MAC_ADDRESS_REGEXP = r"^([0-9A-F]{2}:){5}([0-9A-F]{2})$" + +V1_API_SCHEMA = vol.Schema( + { + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP), + } +) +V3_API_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP), } ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Any(_V3_API_SCHEMA, _V1_API_SCHEMA)}, extra=vol.ALLOW_EXTRA + {DOMAIN: vol.Any(V3_API_SCHEMA, V1_API_SCHEMA)}, extra=vol.ALLOW_EXTRA ) -async def async_setup(hass, hass_config): +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Create a Genius Hub system.""" - kwargs = dict(hass_config[DOMAIN]) + hass.data[DOMAIN] = {} + + kwargs = dict(config[DOMAIN]) if CONF_HOST in kwargs: args = (kwargs.pop(CONF_HOST),) else: args = (kwargs.pop(CONF_TOKEN),) + hub_uid = kwargs.pop(CONF_MAC, None) - hass.data[DOMAIN] = {} - broker = GeniusBroker(hass, args, kwargs) + client = GeniusHub(*args, **kwargs, session=async_get_clientsession(hass)) + + broker = hass.data[DOMAIN]["broker"] = GeniusBroker(hass, client, hub_uid) try: - await broker.client.update() + await client.update() except aiohttp.ClientResponseError as err: _LOGGER.error("Setup failed, check your configuration, %s", err) return False @@ -59,16 +94,8 @@ async def async_setup(hass, hass_config): async_track_time_interval(hass, broker.async_update, SCAN_INTERVAL) - for platform in ["climate", "water_heater"]: - hass.async_create_task( - async_load_platform(hass, platform, DOMAIN, {}, hass_config) - ) - - if broker.client.api_version == 3: # pylint: disable=no-member - for platform in ["sensor", "binary_sensor"]: - hass.async_create_task( - async_load_platform(hass, platform, DOMAIN, {}, hass_config) - ) + for platform in ["climate", "water_heater", "sensor", "binary_sensor"]: + hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) return True @@ -76,25 +103,30 @@ async def async_setup(hass, hass_config): class GeniusBroker: """Container for geniushub client and data.""" - def __init__(self, hass, args, kwargs): + def __init__(self, hass, client, hub_uid) -> None: """Initialize the geniushub client.""" self.hass = hass - self.client = hass.data[DOMAIN]["client"] = GeniusHub( - *args, **kwargs, session=async_get_clientsession(hass) - ) + self.client = client + self._hub_uid = hub_uid + + @property + def hub_uid(self) -> int: + """Return the Hub UID (MAC address).""" + # pylint: disable=no-member + return self._hub_uid if self._hub_uid is not None else self.client.uid - async def async_update(self, now, **kwargs): + async def async_update(self, now, **kwargs) -> None: """Update the geniushub client's data.""" try: await self.client.update() except aiohttp.ClientResponseError as err: - _LOGGER.warning("Update failed, %s", err) + _LOGGER.warning("Update failed, message is: %s", err) return self.make_debug_log_entries() async_dispatcher_send(self.hass, DOMAIN) - def make_debug_log_entries(self): + def make_debug_log_entries(self) -> None: """Make any useful debug log entries.""" # pylint: disable=protected-access _LOGGER.debug( @@ -105,13 +137,13 @@ def make_debug_log_entries(self): class GeniusEntity(Entity): - """Base for all Genius Hub endtities.""" + """Base for all Genius Hub entities.""" - def __init__(self): + def __init__(self) -> None: """Initialize the entity.""" - self._name = None + self._unique_id = self._name = None - async def async_added_to_hass(self) -> Awaitable[None]: + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._refresh) @@ -119,6 +151,11 @@ async def async_added_to_hass(self) -> Awaitable[None]: def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + @property def name(self) -> str: """Return the name of the geniushub entity.""" @@ -128,3 +165,102 @@ def name(self) -> str: def should_poll(self) -> bool: """Return False as geniushub entities should not be polled.""" return False + + +class GeniusDevice(GeniusEntity): + """Base for all Genius Hub devices.""" + + def __init__(self, broker, device) -> None: + """Initialize the Device.""" + super().__init__() + + self._device = device + self._unique_id = f"{broker.hub_uid}_device_{device.id}" + + self._last_comms = self._state_attr = None + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return the device state attributes.""" + + attrs = {} + attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] + if self._last_comms: + attrs["last_comms"] = self._last_comms.isoformat() + + state = dict(self._device.data["state"]) + if "_state" in self._device.data: # only for v3 API + state.update(self._device.data["_state"]) + + attrs["state"] = { + GH_DEVICE_ATTRS[k]: v for k, v in state.items() if k in GH_DEVICE_ATTRS + } + + return attrs + + async def async_update(self) -> None: + """Update an entity's state data.""" + if "_state" in self._device.data: # only for v3 API + self._last_comms = dt_util.utc_from_timestamp( + self._device.data["_state"]["lastComms"] + ) + + +class GeniusZone(GeniusEntity): + """Base for all Genius Hub zones.""" + + def __init__(self, broker, zone) -> None: + """Initialize the Zone.""" + super().__init__() + + self._zone = zone + self._unique_id = f"{broker.hub_uid}_device_{zone.id}" + + self._max_temp = self._min_temp = self._supported_features = None + + @property + def name(self) -> str: + """Return the name of the climate device.""" + return self._zone.name + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return the device state attributes.""" + status = {k: v for k, v in self._zone.data.items() if k in GH_ZONE_ATTRS} + return {"status": status} + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self._zone.data.get("temperature") + + @property + def target_temperature(self) -> float: + """Return the temperature we try to reach.""" + return self._zone.data["setpoint"] + + @property + def min_temp(self) -> float: + """Return max valid temperature that can be set.""" + return self._min_temp + + @property + def max_temp(self) -> float: + """Return max valid temperature that can be set.""" + return self._max_temp + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def supported_features(self) -> int: + """Return the bitmask of supported features.""" + return self._supported_features + + async def async_set_temperature(self, **kwargs) -> None: + """Set a new target temperature for this zone.""" + await self._zone.set_override( + kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600) + ) diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index 105a03bf757c68..33458d049a2841 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -1,52 +1,45 @@ """Support for Genius Hub binary_sensor devices.""" -from typing import Any, Dict - from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusEntity +from . import DOMAIN, GeniusDevice -GH_IS_SWITCH = ["Dual Channel Receiver", "Electric Switch", "Smart Plug"] +GH_STATE_ATTR = "outputOnOff" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Set up the Genius Hub sensor entities.""" - client = hass.data[DOMAIN]["client"] + if discovery_info is None: + return + + broker = hass.data[DOMAIN]["broker"] switches = [ - GeniusBinarySensor(d) for d in client.device_objs if d.type[:21] in GH_IS_SWITCH + GeniusBinarySensor(broker, d, GH_STATE_ATTR) + for d in broker.client.device_objs + if GH_STATE_ATTR in d.data["state"] ] - async_add_entities(switches) + async_add_entities(switches, update_before_add=True) -class GeniusBinarySensor(GeniusEntity, BinarySensorDevice): +class GeniusBinarySensor(GeniusDevice, BinarySensorDevice): """Representation of a Genius Hub binary_sensor.""" - def __init__(self, device) -> None: + def __init__(self, broker, device, state_attr) -> None: """Initialize the binary sensor.""" - super().__init__() + super().__init__(broker, device) + + self._state_attr = state_attr - self._device = device if device.type[:21] == "Dual Channel Receiver": - self._name = f"Dual Channel Receiver {device.id}" + self._name = f"{device.type[:21]} {device.id}" else: self._name = f"{device.type} {device.id}" @property def is_on(self) -> bool: """Return the status of the sensor.""" - return self._device.data["state"]["outputOnOff"] - - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device state attributes.""" - attrs = {} - attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - - # pylint: disable=protected-access - last_comms = self._device._raw["childValues"]["lastComms"]["val"] - if last_comms != 0: - attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() - - return {**attrs} + return self._device.data["state"][self._state_attr] diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index a856e48438fcd6..f27b1cc7f1aebc 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,5 +1,5 @@ """Support for Genius Hub climate devices.""" -from typing import Any, Awaitable, Dict, Optional, List +from typing import Optional, List from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -10,16 +10,9 @@ SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusEntity - -ATTR_DURATION = "duration" - -GH_ZONES = ["radiator"] - -# temperature is repeated here, as it gives access to high-precision temps -GH_STATE_ATTRS = ["mode", "temperature", "type", "occupied", "override"] +from . import DOMAIN, GeniusZone # GeniusHub Zones support: Off, Timer, Override/Boost, Footprint & Linked modes HA_HVAC_TO_GH = {HVAC_MODE_OFF: "off", HVAC_MODE_HEAT: "timer"} @@ -28,78 +21,43 @@ HA_PRESET_TO_GH = {PRESET_ACTIVITY: "footprint", PRESET_BOOST: "override"} GH_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_GH.items()} +GH_ZONES = ["radiator", "wet underfloor"] + async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Set up the Genius Hub climate entities.""" - client = hass.data[DOMAIN]["client"] + if discovery_info is None: + return + + broker = hass.data[DOMAIN]["broker"] - entities = [ - GeniusClimateZone(z) for z in client.zone_objs if z.data["type"] in GH_ZONES - ] - async_add_entities(entities) + async_add_entities( + [ + GeniusClimateZone(broker, z) + for z in broker.client.zone_objs + if z.data["type"] in GH_ZONES + ] + ) -class GeniusClimateZone(GeniusEntity, ClimateDevice): +class GeniusClimateZone(GeniusZone, ClimateDevice): """Representation of a Genius Hub climate device.""" - def __init__(self, zone) -> None: + def __init__(self, broker, zone) -> None: """Initialize the climate device.""" - super().__init__() + super().__init__(broker, zone) - self._zone = zone - if hasattr(self._zone, "occupied"): # has a movement sensor - self._preset_modes = list(HA_PRESET_TO_GH) - else: - self._preset_modes = [PRESET_BOOST] - - @property - def name(self) -> str: - """Return the name of the climate device.""" - return self._zone.name - - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device state attributes.""" - tmp = self._zone.data.items() - return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} + self._max_temp = 28.0 + self._min_temp = 4.0 + self._supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @property def icon(self) -> str: """Return the icon to use in the frontend UI.""" return "mdi:radiator" - @property - def current_temperature(self) -> Optional[float]: - """Return the current temperature.""" - return self._zone.data["temperature"] - - @property - def target_temperature(self) -> float: - """Return the temperature we try to reach.""" - return self._zone.data["setpoint"] - - @property - def min_temp(self) -> float: - """Return max valid temperature that can be set.""" - return 4.0 - - @property - def max_temp(self) -> float: - """Return max valid temperature that can be set.""" - return 28.0 - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE - @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" @@ -118,18 +76,14 @@ def preset_mode(self) -> Optional[str]: @property def preset_modes(self) -> Optional[List[str]]: """Return a list of available preset modes.""" - return self._preset_modes - - async def async_set_temperature(self, **kwargs) -> Awaitable[None]: - """Set a new target temperature for this zone.""" - await self._zone.set_override( - kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600) - ) + if "occupied" in self._zone.data: # if has a movement sensor + return [PRESET_ACTIVITY, PRESET_BOOST] + return [PRESET_BOOST] - async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set a new hvac mode.""" await self._zone.set_mode(HA_HVAC_TO_GH.get(hvac_mode)) - async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set a new preset mode.""" await self._zone.set_mode(HA_PRESET_TO_GH.get(preset_mode, "timer")) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index feedf3be607b71..96497388a4818c 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,7 +3,7 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/integrations/geniushub", "requirements": [ - "geniushub-client==0.6.13" + "geniushub-client==0.6.26" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 82db3d4224e45a..2f5d9bceb8bcda 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -1,13 +1,14 @@ """Support for Genius Hub sensor devices.""" from datetime import timedelta -from typing import Any, Awaitable, Dict +from typing import Any, Dict from homeassistant.const import DEVICE_CLASS_BATTERY -from homeassistant.util.dt import utc_from_timestamp, utcnow +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +import homeassistant.util.dt as dt_util -from . import DOMAIN, GeniusEntity +from . import DOMAIN, GeniusDevice, GeniusEntity -GH_HAS_BATTERY = ["Room Thermostat", "Genius Valve", "Room Sensor", "Radiator Valve"] +GH_STATE_ATTR = "batteryLevel" GH_LEVEL_MAPPING = { "error": "Errors", @@ -16,42 +17,47 @@ } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Set up the Genius Hub sensor entities.""" - client = hass.data[DOMAIN]["client"] + if discovery_info is None: + return - sensors = [GeniusBattery(d) for d in client.device_objs if d.type in GH_HAS_BATTERY] - issues = [GeniusIssue(client, i) for i in list(GH_LEVEL_MAPPING)] + broker = hass.data[DOMAIN]["broker"] + + sensors = [ + GeniusBattery(broker, d, GH_STATE_ATTR) + for d in broker.client.device_objs + if GH_STATE_ATTR in d.data["state"] + ] + issues = [GeniusIssue(broker, i) for i in list(GH_LEVEL_MAPPING)] async_add_entities(sensors + issues, update_before_add=True) -class GeniusBattery(GeniusEntity): +class GeniusBattery(GeniusDevice): """Representation of a Genius Hub sensor.""" - def __init__(self, device) -> None: + def __init__(self, broker, device, state_attr) -> None: """Initialize the sensor.""" - super().__init__() + super().__init__(broker, device) + + self._state_attr = state_attr - self._device = device self._name = f"{device.type} {device.id}" @property def icon(self) -> str: """Return the icon of the sensor.""" - - values = self._device._raw["childValues"] # pylint: disable=protected-access - - last_comms = utc_from_timestamp(values["lastComms"]["val"]) - if "WakeUp_Interval" in values: - interval = timedelta(seconds=values["WakeUp_Interval"]["val"]) - else: - interval = timedelta(minutes=20) - - if last_comms < utcnow() - interval * 3: - return "mdi:battery-unknown" - - battery_level = self._device.data["state"]["batteryLevel"] + if "_state" in self._device.data: # only for v3 API + interval = timedelta( + seconds=self._device.data["_state"].get("wakeupInterval", 30 * 60) + ) + if self._last_comms < dt_util.utcnow() - interval * 3: + return "mdi:battery-unknown" + + battery_level = self._device.data["state"][self._state_attr] if battery_level == 255: return "mdi:battery-unknown" if battery_level < 40: @@ -76,31 +82,19 @@ def unit_of_measurement(self) -> str: @property def state(self) -> str: """Return the state of the sensor.""" - level = self._device.data["state"].get("batteryLevel", 255) + level = self._device.data["state"][self._state_attr] return level if level != 255 else 0 - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device state attributes.""" - attrs = {} - attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - - # pylint: disable=protected-access - last_comms = self._device._raw["childValues"]["lastComms"]["val"] - attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() - - return {**attrs} - class GeniusIssue(GeniusEntity): """Representation of a Genius Hub sensor.""" - def __init__(self, hub, level) -> None: + def __init__(self, broker, level) -> None: """Initialize the sensor.""" super().__init__() - self._hub = hub - self._name = GH_LEVEL_MAPPING[level] + self._hub = broker.client + self._name = f"GeniusHub {GH_LEVEL_MAPPING[level]}" self._level = level self._issues = [] @@ -114,7 +108,7 @@ def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" return {f"{self._level}_list": self._issues} - async def async_update(self) -> Awaitable[None]: + async def async_update(self) -> None: """Process the sensor's state data.""" self._issues = [ i["description"] for i in self._hub.issues if i["level"] == self._level diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 1086160e77c864..cd4f536e14fa12 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -1,27 +1,20 @@ """Support for Genius Hub water_heater devices.""" -from typing import Any, Awaitable, Dict, Optional, List +from typing import List from homeassistant.components.water_heater import ( WaterHeaterDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, ) -from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS +from homeassistant.const import STATE_OFF +from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusEntity +from . import DOMAIN, GeniusZone STATE_AUTO = "auto" STATE_MANUAL = "manual" -GH_HEATERS = ["hot water temperature"] - -GH_SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE -# HA does not have SUPPORT_ON_OFF for water_heater - -GH_MAX_TEMP = 80.0 -GH_MIN_TEMP = 30.0 - -# Genius Hub HW supports only Off, Override/Boost & Timer modes +# Genius Hub HW zones support only Off, Override/Boost & Timer modes HA_OPMODE_TO_GH = {STATE_OFF: "off", STATE_AUTO: "timer", STATE_MANUAL: "override"} GH_STATE_TO_HA = { "off": STATE_OFF, @@ -34,91 +27,49 @@ "linked": None, "other": None, } -GH_STATE_ATTRS = ["type", "override"] + +GH_HEATERS = ["hot water temperature"] async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Set up the Genius Hub water_heater entities.""" - client = hass.data[DOMAIN]["client"] + if discovery_info is None: + return - entities = [ - GeniusWaterHeater(z) for z in client.zone_objs if z.data["type"] in GH_HEATERS - ] + broker = hass.data[DOMAIN]["broker"] - async_add_entities(entities) + async_add_entities( + [ + GeniusWaterHeater(broker, z) + for z in broker.client.zone_objs + if z.data["type"] in GH_HEATERS + ] + ) -class GeniusWaterHeater(GeniusEntity, WaterHeaterDevice): +class GeniusWaterHeater(GeniusZone, WaterHeaterDevice): """Representation of a Genius Hub water_heater device.""" - def __init__(self, boiler) -> None: + def __init__(self, broker, zone) -> None: """Initialize the water_heater device.""" - super().__init__() + super().__init__(broker, zone) - self._boiler = boiler - self._operation_list = list(HA_OPMODE_TO_GH) - - @property - def name(self) -> str: - """Return the name of the water_heater device.""" - return self._boiler.name - - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device state attributes.""" - return { - "status": { - k: v for k, v in self._boiler.data.items() if k in GH_STATE_ATTRS - } - } - - @property - def current_temperature(self) -> Optional[float]: - """Return the current temperature.""" - return self._boiler.data.get("temperature") - - @property - def target_temperature(self) -> float: - """Return the temperature we try to reach.""" - return self._boiler.data["setpoint"] - - @property - def min_temp(self) -> float: - """Return max valid temperature that can be set.""" - return GH_MIN_TEMP - - @property - def max_temp(self) -> float: - """Return max valid temperature that can be set.""" - return GH_MAX_TEMP - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return GH_SUPPORT_FLAGS + self._max_temp = 80.0 + self._min_temp = 30.0 + self._supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE @property def operation_list(self) -> List[str]: """Return the list of available operation modes.""" - return self._operation_list + return list(HA_OPMODE_TO_GH) @property def current_operation(self) -> str: """Return the current operation mode.""" - return GH_STATE_TO_HA[self._boiler.data["mode"]] + return GH_STATE_TO_HA[self._zone.data["mode"]] - async def async_set_operation_mode(self, operation_mode) -> Awaitable[None]: + async def async_set_operation_mode(self, operation_mode) -> None: """Set a new operation mode for this boiler.""" - await self._boiler.set_mode(HA_OPMODE_TO_GH[operation_mode]) - - async def async_set_temperature(self, **kwargs) -> Awaitable[None]: - """Set a new target temperature for this boiler.""" - temperature = kwargs[ATTR_TEMPERATURE] - await self._boiler.set_override(temperature, 3600) # 1 hour + await self._zone.set_mode(HA_OPMODE_TO_GH[operation_mode]) diff --git a/requirements_all.txt b/requirements_all.txt index 2cf2c765e8b0e1..a0f64eeec74b9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -525,7 +525,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.6.13 +geniushub-client==0.6.26 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From 04ead6f273d4c179ded4c7bc0fa293ef86e9a702 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Wed, 2 Oct 2019 18:33:47 +0200 Subject: [PATCH 251/296] move ATTR_MODE to homeassistant.const (#27118) --- homeassistant/components/flux_led/light.py | 3 +-- homeassistant/components/gpsd/sensor.py | 2 +- homeassistant/components/here_travel_time/sensor.py | 2 +- homeassistant/components/homematic/__init__.py | 2 +- homeassistant/components/input_number/__init__.py | 2 +- homeassistant/components/input_text/__init__.py | 2 +- homeassistant/components/lifx/light.py | 3 +-- homeassistant/components/logi_circle/__init__.py | 2 +- homeassistant/components/neato/vacuum.py | 3 +-- homeassistant/components/opentherm_gw/__init__.py | 2 +- homeassistant/components/opentherm_gw/const.py | 1 - homeassistant/components/transport_nsw/sensor.py | 3 +-- homeassistant/components/wink/lock.py | 9 +++++++-- homeassistant/components/xiaomi_miio/fan.py | 9 +++++++-- homeassistant/components/xiaomi_miio/switch.py | 9 +++++++-- homeassistant/components/yeelight/light.py | 3 +-- homeassistant/const.py | 2 ++ 17 files changed, 35 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 23fdb38aa05f7b..0a95de783fa273 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -5,7 +5,7 @@ import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL +from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL, ATTR_MODE from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, @@ -30,7 +30,6 @@ CONF_COLORS = "colors" CONF_SPEED_PCT = "speed_pct" CONF_TRANSITION = "transition" -ATTR_MODE = "mode" DOMAIN = "flux_led" diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index ab4545256ae3e3..197e424ce86e69 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -7,6 +7,7 @@ from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, + ATTR_MODE, CONF_HOST, CONF_PORT, CONF_NAME, @@ -19,7 +20,6 @@ ATTR_CLIMB = "climb" ATTR_ELEVATION = "elevation" ATTR_GPS_TIME = "gps_time" -ATTR_MODE = "mode" ATTR_SPEED = "speed" DEFAULT_HOST = "localhost" diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 8fd4b4fe94a5cb..b752b82d08750b 100755 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -11,6 +11,7 @@ ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + ATTR_MODE, CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM, @@ -77,7 +78,6 @@ ATTR_ORIGIN = "origin" ATTR_DESTINATION = "destination" -ATTR_MODE = "mode" ATTR_UNIT_SYSTEM = CONF_UNIT_SYSTEM ATTR_TRAFFIC_MODE = CONF_TRAFFIC_MODE diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 598e3765612420..cd791434f90072 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -7,6 +7,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_MODE, ATTR_NAME, CONF_HOST, CONF_HOSTS, @@ -47,7 +48,6 @@ ATTR_INTERFACE = "interface" ATTR_ERRORCODE = "error" ATTR_MESSAGE = "message" -ATTR_MODE = "mode" ATTR_TIME = "time" ATTR_UNIQUE_ID = "unique_id" ATTR_PARAMSET_KEY = "paramset_key" diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 007ed6517efe47..9b4d5a961ba3a0 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -7,6 +7,7 @@ from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + ATTR_MODE, CONF_ICON, CONF_NAME, CONF_MODE, @@ -32,7 +33,6 @@ ATTR_MIN = "min" ATTR_MAX = "max" ATTR_STEP = "step" -ATTR_MODE = "mode" SERVICE_SET_VALUE = "set_value" SERVICE_INCREMENT = "increment" diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 41d78e6e7c540c..1b4670cf1e6443 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -7,6 +7,7 @@ from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + ATTR_MODE, CONF_ICON, CONF_NAME, CONF_MODE, @@ -30,7 +31,6 @@ ATTR_MIN = "min" ATTR_MAX = "max" ATTR_PATTERN = "pattern" -ATTR_MODE = "mode" SERVICE_SET_VALUE = "set_value" diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index ed26db3d49e535..d183dcb0fa2627 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -33,7 +33,7 @@ Light, preprocess_turn_on_alternatives, ) -from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr @@ -77,7 +77,6 @@ SERVICE_EFFECT_STOP = "lifx_effect_stop" ATTR_POWER_ON = "power_on" -ATTR_MODE = "mode" ATTR_PERIOD = "period" ATTR_CYCLES = "cycles" ATTR_SPREAD = "spread" diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 12484a655d635f..f7ed3a73fce033 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components.camera import ATTR_FILENAME, CAMERA_SERVICE_SCHEMA from homeassistant.const import ( + ATTR_MODE, CONF_MONITORED_CONDITIONS, CONF_SENSORS, EVENT_HOMEASSISTANT_STOP, @@ -42,7 +43,6 @@ SERVICE_LIVESTREAM_SNAPSHOT = "livestream_snapshot" SERVICE_LIVESTREAM_RECORD = "livestream_record" -ATTR_MODE = "mode" ATTR_VALUE = "value" ATTR_DURATION = "duration" diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 93fe285dcfdb69..f284b2eda1ed25 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -27,7 +27,7 @@ SUPPORT_STOP, StateVacuumDevice, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import extract_entity_ids @@ -66,7 +66,6 @@ ATTR_CLEAN_SUSP_COUNT = "clean_suspension_count" ATTR_CLEAN_SUSP_TIME = "clean_suspension_time" -ATTR_MODE = "mode" ATTR_NAVIGATION = "navigation" ATTR_CATEGORY = "category" ATTR_ZONE = "zone" diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 0c145963653c3e..a32c375ac65258 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ATTR_DATE, ATTR_ID, + ATTR_MODE, ATTR_TEMPERATURE, ATTR_TIME, CONF_DEVICE, @@ -28,7 +29,6 @@ from .const import ( ATTR_GW_ID, - ATTR_MODE, ATTR_LEVEL, ATTR_DHW_OVRD, CONF_CLIMATE, diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 77b0bf9b313c8e..60042b92867ca4 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -4,7 +4,6 @@ from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS ATTR_GW_ID = "gateway_id" -ATTR_MODE = "mode" ATTR_LEVEL = "level" ATTR_DHW_OVRD = "dhw_override" diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 5f08d0a4750718..9d0610c139e910 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -7,7 +7,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_API_KEY, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_MODE, CONF_NAME, CONF_API_KEY, ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -17,7 +17,6 @@ ATTR_DELAY = "delay" ATTR_REAL_TIME = "real_time" ATTR_DESTINATION = "destination" -ATTR_MODE = "mode" ATTRIBUTION = "Data provided by Transport NSW" diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py index 0d4d373b2b65b0..5246fb49eed23e 100644 --- a/homeassistant/components/wink/lock.py +++ b/homeassistant/components/wink/lock.py @@ -4,7 +4,13 @@ import voluptuous as vol from homeassistant.components.lock import LockDevice -from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, ATTR_NAME, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_CODE, + ATTR_ENTITY_ID, + ATTR_MODE, + ATTR_NAME, + STATE_UNKNOWN, +) import homeassistant.helpers.config_validation as cv from . import DOMAIN, WinkDevice @@ -20,7 +26,6 @@ ATTR_ENABLED = "enabled" ATTR_SENSITIVITY = "sensitivity" -ATTR_MODE = "mode" ALARM_SENSITIVITY_MAP = { "low": 0.2, diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index c6ca6db32fbce6..67dc12565d8680 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -12,7 +12,13 @@ SUPPORT_SET_SPEED, DOMAIN, ) -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_TOKEN, ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_MODE, + CONF_NAME, + CONF_HOST, + CONF_TOKEN, + ATTR_ENTITY_ID, +) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -75,7 +81,6 @@ ATTR_TEMPERATURE = "temperature" ATTR_HUMIDITY = "humidity" ATTR_AIR_QUALITY_INDEX = "aqi" -ATTR_MODE = "mode" ATTR_FILTER_HOURS_USED = "filter_hours_used" ATTR_FILTER_LIFE = "filter_life_remaining" ATTR_FAVORITE_LEVEL = "favorite_level" diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 5f79652621bab3..7fa1638253cd24 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -6,7 +6,13 @@ import voluptuous as vol from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_MODE, + CONF_HOST, + CONF_NAME, + CONF_TOKEN, +) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -44,7 +50,6 @@ ATTR_TEMPERATURE = "temperature" ATTR_LOAD_POWER = "load_power" ATTR_MODEL = "model" -ATTR_MODE = "mode" ATTR_POWER_MODE = "power_mode" ATTR_WIFI_LED = "wifi_led" ATTR_POWER_PRICE = "power_price" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index b47cdb981612e4..ab63e6fb319455 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -11,7 +11,7 @@ color_temperature_mired_to_kelvin as mired_to_kelvin, color_temperature_kelvin_to_mired as kelvin_to_mired, ) -from homeassistant.const import CONF_HOST, ATTR_ENTITY_ID, CONF_NAME +from homeassistant.const import CONF_HOST, ATTR_ENTITY_ID, ATTR_MODE, CONF_NAME from homeassistant.core import callback from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -64,7 +64,6 @@ SUPPORT_YEELIGHT_RGB = SUPPORT_YEELIGHT_WHITE_TEMP | SUPPORT_COLOR -ATTR_MODE = "mode" ATTR_MINUTES = "minutes" SERVICE_SET_MODE = "set_mode" diff --git a/homeassistant/const.py b/homeassistant/const.py index 9aa4544f5c34e7..7d8a68a9707ad4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -271,6 +271,8 @@ # Location of the device/sensor ATTR_LOCATION = "location" +ATTR_MODE = "mode" + ATTR_BATTERY_CHARGING = "battery_charging" ATTR_BATTERY_LEVEL = "battery_level" ATTR_WAKEUP = "wake_up_interval" From d4a67e3a300944c53f8f2aeffc1091e199437bf6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Oct 2019 18:34:07 +0200 Subject: [PATCH 252/296] Update documentation link URL for integrations (part2) (#27117) --- .github/ISSUE_TEMPLATE.md | 4 ++-- .github/ISSUE_TEMPLATE/Bug_report.md | 4 ++-- README.rst | 2 +- homeassistant/components/ambiclimate/.translations/en.json | 2 +- homeassistant/components/deconz/services.yaml | 2 +- homeassistant/components/dialogflow/config_flow.py | 2 +- homeassistant/components/geofency/config_flow.py | 2 +- homeassistant/components/gpslogger/config_flow.py | 2 +- homeassistant/components/honeywell/climate.py | 2 +- homeassistant/components/ifttt/config_flow.py | 2 +- homeassistant/components/izone/__init__.py | 2 +- homeassistant/components/life360/config_flow.py | 2 +- homeassistant/components/locative/config_flow.py | 2 +- homeassistant/components/logi_circle/.translations/en.json | 2 +- homeassistant/components/mailgun/config_flow.py | 2 +- homeassistant/components/nest/.translations/en.json | 2 +- homeassistant/components/opentherm_gw/services.yaml | 4 ++-- homeassistant/components/owntracks/config_flow.py | 2 +- homeassistant/components/plaato/config_flow.py | 4 +++- homeassistant/components/point/.translations/en.json | 2 +- homeassistant/components/ps4/.translations/en.json | 6 +++--- homeassistant/components/smarthab/__init__.py | 2 +- homeassistant/components/smarthab/cover.py | 2 +- homeassistant/components/smarthab/light.py | 2 +- homeassistant/components/smartthings/config_flow.py | 2 +- homeassistant/components/somfy/__init__.py | 2 +- homeassistant/components/toon/.translations/en.json | 2 +- homeassistant/components/traccar/config_flow.py | 2 +- homeassistant/components/twilio/config_flow.py | 2 +- homeassistant/components/zha/core/__init__.py | 2 +- homeassistant/components/zha/core/channels/__init__.py | 2 +- homeassistant/components/zha/core/channels/closures.py | 2 +- homeassistant/components/zha/core/channels/general.py | 2 +- .../components/zha/core/channels/homeautomation.py | 2 +- homeassistant/components/zha/core/channels/hvac.py | 2 +- homeassistant/components/zha/core/channels/lighting.py | 2 +- homeassistant/components/zha/core/channels/lightlink.py | 2 +- .../components/zha/core/channels/manufacturerspecific.py | 2 +- homeassistant/components/zha/core/channels/measurement.py | 2 +- homeassistant/components/zha/core/channels/protocol.py | 2 +- homeassistant/components/zha/core/channels/security.py | 2 +- homeassistant/components/zha/core/channels/smartenergy.py | 2 +- homeassistant/components/zha/core/device.py | 2 +- homeassistant/components/zha/core/discovery.py | 2 +- homeassistant/components/zha/core/gateway.py | 2 +- homeassistant/components/zha/core/helpers.py | 2 +- homeassistant/components/zha/core/patches.py | 2 +- homeassistant/components/zha/core/registries.py | 2 +- homeassistant/config.py | 4 ++-- .../templates/integration/integration/manifest.json | 2 +- 50 files changed, 58 insertions(+), 56 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 28dade82d9878e..1af7fc0490e679 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -23,9 +23,9 @@ Please provide details about your environment. --> -**Component/platform:** +**Integration:** diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 3b962f38caf2ad..885164d7a3469f 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -29,9 +29,9 @@ about: Create a report to help us improve Please provide details about your environment. --> -**Component/platform:** +**Integration:** diff --git a/README.rst b/README.rst index 08f20778d701af..ae9531456fdbba 100644 --- a/README.rst +++ b/README.rst @@ -32,4 +32,4 @@ of a component, check the `Home Assistant help section Mode to set on the GPIO pin. Values 0 through 6 are accepted for both GPIOs, 7 is only accepted for GPIO "B". - See https://www.home-assistant.io/components/opentherm_gw/#gpio-modes for an explanation of the values. + See https://www.home-assistant.io/integrations/opentherm_gw/#gpio-modes for an explanation of the values. example: '5' set_led_mode: @@ -79,7 +79,7 @@ set_led_mode: mode: description: > The function to assign to the LED. One of "R", "X", "T", "B", "O", "F", "H", "W", "C", "E", "M" or "P". - See https://www.home-assistant.io/components/opentherm_gw/#led-modes for an explanation of the values. + See https://www.home-assistant.io/integrations/opentherm_gw/#led-modes for an explanation of the values. example: 'F' set_max_modulation: diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 93a75d44e9b3fb..67553ef608ffdb 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -58,7 +58,7 @@ async def async_step_user(self, user_input=None): "android_url": "https://play.google.com/store/apps/details?" "id=org.owntracks.android", "ios_url": "https://itunes.apple.com/us/app/owntracks/id692424691?mt=8", - "docs_url": "https://www.home-assistant.io/components/owntracks/", + "docs_url": "https://www.home-assistant.io/integrations/owntracks/", }, ) diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 2790d9a93acb7b..59cb270c616af4 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -3,5 +3,7 @@ from .const import DOMAIN config_entry_flow.register_webhook_flow( - DOMAIN, "Webhook", {"docs_url": "https://www.home-assistant.io/components/plaato/"} + DOMAIN, + "Webhook", + {"docs_url": "https://www.home-assistant.io/integrations/plaato/"}, ) diff --git a/homeassistant/components/point/.translations/en.json b/homeassistant/components/point/.translations/en.json index 705ac59b98d010..25f0545c340942 100644 --- a/homeassistant/components/point/.translations/en.json +++ b/homeassistant/components/point/.translations/en.json @@ -5,7 +5,7 @@ "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize url.", "external_setup": "Point successfully configured from another flow.", - "no_flows": "You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/point/)." + "no_flows": "You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/point/)." }, "create_entry": { "default": "Successfully authenticated with Minut for your Point device(s)" diff --git a/homeassistant/components/ps4/.translations/en.json b/homeassistant/components/ps4/.translations/en.json index 756eb65d4f7c58..3a7223ade29d99 100644 --- a/homeassistant/components/ps4/.translations/en.json +++ b/homeassistant/components/ps4/.translations/en.json @@ -4,8 +4,8 @@ "credential_error": "Error fetching credentials.", "devices_configured": "All devices found are already configured.", "no_devices_found": "No PlayStation 4 devices found on the network.", - "port_987_bind_error": "Could not bind to port 987. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", - "port_997_bind_error": "Could not bind to port 997. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info." + "port_987_bind_error": "Could not bind to port 987. Refer to the [documentation](https://www.home-assistant.io/integrations/ps4/) for additional info.", + "port_997_bind_error": "Could not bind to port 997. Refer to the [documentation](https://www.home-assistant.io/integrations/ps4/) for additional info." }, "error": { "credential_timeout": "Credential service timed out. Press submit to restart.", @@ -25,7 +25,7 @@ "name": "Name", "region": "Region" }, - "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", + "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed. Refer to the [documentation](https://www.home-assistant.io/integrations/ps4/) for additional info.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index 198a5e9cabcc1e..7206bea110ba38 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -2,7 +2,7 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/smarthab/ +https://home-assistant.io/integrations/smarthab/ """ import logging diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py index 2ae9cadf1acc75..3d5b4259aa9cf5 100644 --- a/homeassistant/components/smarthab/cover.py +++ b/homeassistant/components/smarthab/cover.py @@ -2,7 +2,7 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/smarthab/ +https://home-assistant.io/integrations/smarthab/ """ import logging from datetime import timedelta diff --git a/homeassistant/components/smarthab/light.py b/homeassistant/components/smarthab/light.py index 0f7b3c9ef80785..a8a55dea48a739 100644 --- a/homeassistant/components/smarthab/light.py +++ b/homeassistant/components/smarthab/light.py @@ -2,7 +2,7 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/smarthab/ +https://home-assistant.io/integrations/smarthab/ """ import logging from datetime import timedelta diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index a3ca8fc7629991..54c9f815008e16 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -176,7 +176,7 @@ def _show_step_user(self, errors): errors=errors, description_placeholders={ "token_url": "https://account.smartthings.com/tokens", - "component_url": "https://www.home-assistant.io/components/smartthings/", + "component_url": "https://www.home-assistant.io/integrations/smartthings/", }, ) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 8c7edb26d46f39..2c7c71d7a69686 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -2,7 +2,7 @@ Support for Somfy hubs. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/somfy/ +https://home-assistant.io/integrations/somfy/ """ import logging from datetime import timedelta diff --git a/homeassistant/components/toon/.translations/en.json b/homeassistant/components/toon/.translations/en.json index cea3146a3a557f..dde5165c5c17f7 100644 --- a/homeassistant/components/toon/.translations/en.json +++ b/homeassistant/components/toon/.translations/en.json @@ -4,7 +4,7 @@ "client_id": "The client ID from the configuration is invalid.", "client_secret": "The client secret from the configuration is invalid.", "no_agreements": "This account has no Toon displays.", - "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/).", + "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/toon/).", "unknown_auth_fail": "Unexpected error occured, while authenticating." }, "error": { diff --git a/homeassistant/components/traccar/config_flow.py b/homeassistant/components/traccar/config_flow.py index cc3f1f2372771e..4bd75910163fdd 100644 --- a/homeassistant/components/traccar/config_flow.py +++ b/homeassistant/components/traccar/config_flow.py @@ -6,5 +6,5 @@ config_entry_flow.register_webhook_flow( DOMAIN, "Traccar Webhook", - {"docs_url": "https://www.home-assistant.io/components/traccar/"}, + {"docs_url": "https://www.home-assistant.io/integrations/traccar/"}, ) diff --git a/homeassistant/components/twilio/config_flow.py b/homeassistant/components/twilio/config_flow.py index 1408e05e738edf..dad8e0bf4966a3 100644 --- a/homeassistant/components/twilio/config_flow.py +++ b/homeassistant/components/twilio/config_flow.py @@ -9,6 +9,6 @@ "Twilio Webhook", { "twilio_url": "https://www.twilio.com/docs/glossary/what-is-a-webhook", - "docs_url": "https://www.home-assistant.io/components/twilio/", + "docs_url": "https://www.home-assistant.io/integrations/twilio/", }, ) diff --git a/homeassistant/components/zha/core/__init__.py b/homeassistant/components/zha/core/__init__.py index 145b725fc7968b..1873cd7dc55be3 100644 --- a/homeassistant/components/zha/core/__init__.py +++ b/homeassistant/components/zha/core/__init__.py @@ -2,7 +2,7 @@ Core module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ # flake8: noqa diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 3d4a03fb0acac0..37b0bec207b986 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -2,7 +2,7 @@ Channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import asyncio from concurrent.futures import TimeoutError as Timeout diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 378be778e6f565..16592c9a8df029 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -2,7 +2,7 @@ Closures channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index f67ee2fb75ac77..7afde3e5f781fd 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -2,7 +2,7 @@ General channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 7a5f0161fb4385..dda6c1f4c13f3b 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -2,7 +2,7 @@ Home automation channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 2f6e6c1b3e8390..14d982ab1e8e9f 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -2,7 +2,7 @@ HVAC channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index d8f769a3e24399..272fa28905cfb9 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -2,7 +2,7 @@ Lighting channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index 99fed7d5d68617..7cd2134988d367 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -2,7 +2,7 @@ Lightlink channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index e15acdaf5e31bd..31dd5cd63d14ea 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -2,7 +2,7 @@ Manufacturer specific channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 94d885592eb39c..369ecb69aa1020 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -2,7 +2,7 @@ Measurement channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/protocol.py b/homeassistant/components/zha/core/channels/protocol.py index b9785068f21d46..aa463392e557c1 100644 --- a/homeassistant/components/zha/core/channels/protocol.py +++ b/homeassistant/components/zha/core/channels/protocol.py @@ -2,7 +2,7 @@ Protocol channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 25c11a9fd4f1cd..e4840dae86dabc 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -2,7 +2,7 @@ Security channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 8e2fa7e3d5a3f2..c7de2943691a5f 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -2,7 +2,7 @@ Smart energy channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 82d20ff78c207e..e9e2c3b7ea6aaf 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -2,7 +2,7 @@ Device for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import asyncio from datetime import timedelta diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 80642a373da7e4..622adead803f1c 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -2,7 +2,7 @@ Device discovery functions for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index d2f842956dabef..a64e8cf7fd98ed 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -2,7 +2,7 @@ Virtual gateway for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import asyncio diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index b07658e72d01eb..88a472716cc580 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -2,7 +2,7 @@ Helpers for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import asyncio import collections diff --git a/homeassistant/components/zha/core/patches.py b/homeassistant/components/zha/core/patches.py index d64839026025ea..a4e84e83105805 100644 --- a/homeassistant/components/zha/core/patches.py +++ b/homeassistant/components/zha/core/patches.py @@ -2,7 +2,7 @@ Patch functions for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index db7e89dce822c7..43ddc888d2fda0 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -2,7 +2,7 @@ Mapping registries for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import collections diff --git a/homeassistant/config.py b/homeassistant/config.py index 0e840e1d003062..97c996d9e59380 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -60,7 +60,7 @@ DATA_PERSISTENT_ERRORS = "bootstrap_persistent_errors" RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") RE_ASCII = re.compile(r"\033\[[^m]*m") -HA_COMPONENT_URL = "[{}](https://home-assistant.io/components/{}/)" +HA_COMPONENT_URL = "[{}](https://home-assistant.io/integrations/{}/)" YAML_CONFIG_FILE = "configuration.yaml" VERSION_FILE = ".HA_VERSION" CONFIG_DIR_NAME = ".homeassistant" @@ -462,7 +462,7 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: if domain != CONF_CORE: message += ( "Please check the docs at " - "https://home-assistant.io/components/{}/".format(domain) + "https://home-assistant.io/integrations/{}/".format(domain) ) return message diff --git a/script/scaffold/templates/integration/integration/manifest.json b/script/scaffold/templates/integration/integration/manifest.json index cb4ecac61fb76c..0bc54519ce9de8 100644 --- a/script/scaffold/templates/integration/integration/manifest.json +++ b/script/scaffold/templates/integration/integration/manifest.json @@ -2,7 +2,7 @@ "domain": "NEW_DOMAIN", "name": "NEW_NAME", "config_flow": false, - "documentation": "https://www.home-assistant.io/components/NEW_DOMAIN", + "documentation": "https://www.home-assistant.io/integrations/NEW_DOMAIN", "requirements": [], "ssdp": {}, "homekit": {}, From 9c49b8dfc1b9723abe77fb7bb975f94b5233ad00 Mon Sep 17 00:00:00 2001 From: Felix Eckhofer Date: Wed, 2 Oct 2019 18:34:27 +0200 Subject: [PATCH 253/296] Fix generated comment in CODEOWNERS (#27115) codeowners.py was moved from `/script/manifest/` to `/script/hassfest/` in e8343452cd4702a61166fd74d78323bf95092f7c. --- CODEOWNERS | 2 +- script/hassfest/codeowners.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index f5cd03882c5944..2bfebf145dfada 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,4 +1,4 @@ -# This file is generated by script/manifest/codeowners.py +# This file is generated by script/hassfest/codeowners.py # People marked here will be automatically requested for a review # when the code that they own is touched. # https://github.com/blog/2392-introducing-code-owners diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 1341bd75d1b0cf..6f63fab3fdb8ba 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -4,7 +4,7 @@ from .model import Integration, Config BASE = """ -# This file is generated by script/manifest/codeowners.py +# This file is generated by script/hassfest/codeowners.py # People marked here will be automatically requested for a review # when the code that they own is touched. # https://github.com/blog/2392-introducing-code-owners From 0eb1d490467c40a6d46a6b94851548b1a5ef2eb8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Oct 2019 20:52:15 +0200 Subject: [PATCH 254/296] Disable flaky/slow test (#27125) --- tests/components/ecobee/test_config_flow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 7b4d1f96a3761c..4008e6a17b18b4 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the ecobee config flow.""" +import pytest from unittest.mock import patch from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN @@ -116,6 +117,7 @@ async def test_token_request_fails(hass): assert result["description_placeholders"] == {"pin": "test-pin"} +@pytest.mark.skip(reason="Flaky/slow") async def test_import_flow_triggered_but_no_ecobee_conf(hass): """Test expected result if import flow triggers but ecobee.conf doesn't exist.""" flow = config_flow.EcobeeFlowHandler() From 09c5b9feb35b70c96848b8796b6d4a747de998f6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 2 Oct 2019 21:43:14 +0200 Subject: [PATCH 255/296] UniFi - Try to handle when UniFi erroneously marks offline client as wired (#26960) * Add controls to catch when client goes offline and UniFi bug marks client as wired * Device trackers shouldn't jump between going away and home * POE control shouldn't add normally wireless clients as POE control switches --- homeassistant/components/unifi/__init__.py | 55 +++++++++++++++++-- homeassistant/components/unifi/const.py | 1 + homeassistant/components/unifi/controller.py | 24 ++++++++ .../components/unifi/device_tracker.py | 31 ++++++++--- homeassistant/components/unifi/switch.py | 5 +- tests/components/unifi/test_controller.py | 14 +++-- tests/components/unifi/test_device_tracker.py | 48 +++++++++++++++- tests/components/unifi/test_switch.py | 5 +- 8 files changed, 161 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index db6358285296a3..5b43289e403b13 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,14 +1,13 @@ """Support for devices connected to UniFi POE.""" import voluptuous as vol -from homeassistant.components.unifi.config_flow import ( - get_controller_id_from_config_entry, -) from homeassistant.const import CONF_HOST +from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC import homeassistant.helpers.config_validation as cv +from .config_flow import get_controller_id_from_config_entry from .const import ( ATTR_MANUFACTURER, CONF_BLOCK_CLIENT, @@ -20,9 +19,14 @@ CONF_SSID_FILTER, DOMAIN, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from .controller import UniFiController +SAVE_DELAY = 10 +STORAGE_KEY = "unifi_data" +STORAGE_VERSION = 1 + CONF_CONTROLLERS = "controllers" CONTROLLER_SCHEMA = vol.Schema( @@ -61,6 +65,9 @@ async def async_setup(hass, config): if DOMAIN in config: hass.data[UNIFI_CONFIG] = config[DOMAIN][CONF_CONTROLLERS] + hass.data[UNIFI_WIRELESS_CLIENTS] = wireless_clients = UnifiWirelessClients(hass) + await wireless_clients.async_load() + return True @@ -70,9 +77,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = {} controller = UniFiController(hass, config_entry) - controller_id = get_controller_id_from_config_entry(config_entry) - hass.data[DOMAIN][controller_id] = controller if not await controller.async_setup(): @@ -99,3 +104,43 @@ async def async_unload_entry(hass, config_entry): controller_id = get_controller_id_from_config_entry(config_entry) controller = hass.data[DOMAIN].pop(controller_id) return await controller.async_reset() + + +class UnifiWirelessClients: + """Class to store clients known to be wireless. + + This is needed since wireless devices going offline might get marked as wired by UniFi. + """ + + def __init__(self, hass): + """Set up client storage.""" + self.hass = hass + self.data = {} + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + async def async_load(self): + """Load data from file.""" + data = await self._store.async_load() + + if data is not None: + self.data = data + + @callback + def get_data(self, config_entry): + """Get data related to a specific controller.""" + controller_id = get_controller_id_from_config_entry(config_entry) + data = self.data.get(controller_id, {"wireless_devices": []}) + return set(data["wireless_devices"]) + + @callback + def update_data(self, data, config_entry): + """Update data and schedule to save to file.""" + controller_id = get_controller_id_from_config_entry(config_entry) + self.data[controller_id] = {"wireless_devices": list(data)} + + self._store.async_delay_save(self._data_to_save, SAVE_DELAY) + + @callback + def _data_to_save(self): + """Return data of UniFi wireless clients to store in a file.""" + return self.data diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 4522ac4254a0c9..eac14735074109 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -10,6 +10,7 @@ CONF_SITE_ID = "site" UNIFI_CONFIG = "unifi_config" +UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index b29b088a815f51..ffea98b90502ff 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -36,6 +36,7 @@ DOMAIN, LOGGER, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from .errors import AuthenticationRequired, CannotConnect @@ -50,6 +51,7 @@ def __init__(self, hass, config_entry): self.available = True self.api = None self.progress = None + self.wireless_clients = None self._site_name = None self._site_role = None @@ -128,6 +130,22 @@ def signal_options_update(self): """Event specific per UniFi entry to signal new options.""" return f"unifi-options-{CONTROLLER_ID.format(host=self.host, site=self.site)}" + def update_wireless_clients(self): + """Update set of known to be wireless clients.""" + new_wireless_clients = set() + + for client_id in self.api.clients: + if ( + client_id not in self.wireless_clients + and not self.api.clients[client_id].is_wired + ): + new_wireless_clients.add(client_id) + + if new_wireless_clients: + self.wireless_clients |= new_wireless_clients + unifi_wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS] + unifi_wireless_clients.update_data(self.wireless_clients, self.config_entry) + async def request_update(self): """Request an update.""" if self.progress is not None: @@ -170,6 +188,8 @@ async def async_update(self): LOGGER.info("Reconnected to controller %s", self.host) self.available = True + self.update_wireless_clients() + async_dispatcher_send(self.hass, self.signal_update) async def async_setup(self): @@ -197,6 +217,10 @@ async def async_setup(self): LOGGER.error("Unknown error connecting with UniFi controller: %s", err) return False + wireless_clients = hass.data[UNIFI_WIRELESS_CLIENTS] + self.wireless_clients = wireless_clients.get_data(self.config_entry) + self.update_wireless_clients() + self.import_configuration() self.config_entry.add_update_listener(self.async_options_updated) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index ad04b8a0eb37da..48b19d7bada4ca 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -26,7 +26,6 @@ "ip", "is_11r", "is_guest", - "is_wired", "mac", "name", "noted", @@ -121,6 +120,7 @@ def __init__(self, client, controller): """Set up tracked client.""" self.client = client self.controller = controller + self.is_wired = self.client.mac not in controller.wireless_clients @property def entity_registry_enabled_default(self): @@ -129,13 +129,13 @@ def entity_registry_enabled_default(self): return False if ( - not self.client.is_wired + not self.is_wired and self.controller.option_ssid_filter and self.client.essid not in self.controller.option_ssid_filter ): return False - if not self.controller.option_track_wired_clients and self.client.is_wired: + if not self.controller.option_track_wired_clients and self.is_wired: return False return True @@ -145,18 +145,31 @@ async def async_added_to_hass(self): LOGGER.debug("New UniFi client tracker %s (%s)", self.name, self.client.mac) async def async_update(self): - """Synchronize state with controller.""" + """Synchronize state with controller. + + Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired. + """ LOGGER.debug( "Updating UniFi tracked client %s (%s)", self.entity_id, self.client.mac ) await self.controller.request_update() + if self.is_wired and self.client.mac in self.controller.wireless_clients: + self.is_wired = False + @property def is_connected(self): - """Return true if the client is connected to the network.""" - if ( - dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.client.last_seen)) - ) < self.controller.option_detection_time: + """Return true if the client is connected to the network. + + If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired. + """ + if self.is_wired == self.client.is_wired and ( + ( + dt_util.utcnow() + - dt_util.utc_from_timestamp(float(self.client.last_seen)) + ) + < self.controller.option_detection_time + ): return True return False @@ -195,6 +208,8 @@ def device_state_attributes(self): if variable in self.client.raw: attributes[variable] = self.client.raw[variable] + attributes["is_wired"] = self.is_wired + return attributes diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 4f757102d530e8..f0183a7ecb3951 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -88,7 +88,7 @@ def update_items(controller, async_add_entities, switches, switches_off): new_switches.append(switches[block_client_id]) LOGGER.debug("New UniFi Block switch %s (%s)", client.hostname, client.mac) - # control poe + # control POE for client_id in controller.api.clients: poe_client_id = f"poe-{client_id}" @@ -108,9 +108,10 @@ def update_items(controller, async_add_entities, switches, switches_off): pass # Network device with active POE elif ( - not client.is_wired + client_id in controller.wireless_clients or client.sw_mac not in devices or not devices[client.sw_mac].ports[client.sw_port].port_poe + or not devices[client.sw_mac].ports[client.sw_port].poe_enable or controller.mac == client.mac ): continue diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index b28044bc3c7476..e73719205f7698 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -8,6 +8,7 @@ CONF_CONTROLLER, CONF_SITE_ID, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from homeassistant.const import ( CONF_HOST, @@ -49,7 +50,8 @@ async def test_controller_setup(): controller.CONF_DETECTION_TIME: 30, controller.CONF_SSID_FILTER: ["ssid"], } - ] + ], + UNIFI_WIRELESS_CLIENTS: Mock(), } entry = Mock() entry.data = ENTRY_CONFIG @@ -57,6 +59,7 @@ async def test_controller_setup(): api = Mock() api.initialize.return_value = mock_coro(True) api.sites.return_value = mock_coro(CONTROLLER_SITES) + api.clients = [] unifi_controller = controller.UniFiController(hass, entry) @@ -100,7 +103,8 @@ async def test_controller_site(): async def test_controller_mac(): """Test that it is possible to identify controller mac.""" hass = Mock() - hass.data = {UNIFI_CONFIG: {}} + hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} + hass.data[UNIFI_WIRELESS_CLIENTS].get_data.return_value = set() entry = Mock() entry.data = ENTRY_CONFIG entry.options = {} @@ -123,7 +127,7 @@ async def test_controller_mac(): async def test_controller_no_mac(): """Test that it works to not find the controllers mac.""" hass = Mock() - hass.data = {UNIFI_CONFIG: {}} + hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} entry = Mock() entry.data = ENTRY_CONFIG entry.options = {} @@ -133,6 +137,7 @@ async def test_controller_no_mac(): api.initialize.return_value = mock_coro(True) api.clients = {"client1": client} api.sites.return_value = mock_coro(CONTROLLER_SITES) + api.clients = {} unifi_controller = controller.UniFiController(hass, entry) @@ -195,13 +200,14 @@ async def test_reset_if_entry_had_wrong_auth(): async def test_reset_unloads_entry_if_setup(): """Calling reset when the entry has been setup.""" hass = Mock() - hass.data = {UNIFI_CONFIG: {}} + hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} entry = Mock() entry.data = ENTRY_CONFIG entry.options = {} api = Mock() api.initialize.return_value = mock_coro(True) api.sites.return_value = mock_coro(CONTROLLER_SITES) + api.clients = [] unifi_controller = controller.UniFiController(hass, entry) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 760e1e4fa4c50c..3a2b37487afcdd 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,9 +1,11 @@ """The tests for the UniFi device tracker platform.""" from collections import deque from copy import copy -from unittest.mock import Mock + from datetime import timedelta +from asynctest import Mock + import pytest from aiounifi.clients import Clients, ClientsAll @@ -19,6 +21,7 @@ CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID as CONF_CONTROLLER_ID, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from homeassistant.const import ( CONF_HOST, @@ -96,7 +99,7 @@ CONF_PASSWORD: "mock-pswd", CONF_PORT: 1234, CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: True, + CONF_VERIFY_SSL: False, } ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} @@ -108,7 +111,9 @@ def mock_controller(hass): """Mock a UniFi Controller.""" hass.data[UNIFI_CONFIG] = {} + hass.data[UNIFI_WIRELESS_CLIENTS] = Mock() controller = unifi.UniFiController(hass, None) + controller.wireless_clients = set() controller.api = Mock() controller.mock_requests = [] @@ -253,6 +258,45 @@ async def test_tracked_devices(hass, mock_controller): assert device_1 is None +async def test_wireless_client_go_wired_issue(hass, mock_controller): + """Test the solution to catch wireless device go wired UniFi issue. + + UniFi has a known issue that when a wireless device goes away it sometimes gets marked as wired. + """ + client_1_client = copy(CLIENT_1) + client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + mock_controller.mock_client_responses.append([client_1_client]) + mock_controller.mock_device_responses.append({}) + + await setup_controller(hass, mock_controller) + assert len(mock_controller.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + assert client_1.state == "home" + + client_1_client["is_wired"] = True + client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + mock_controller.mock_client_responses.append([client_1_client]) + mock_controller.mock_device_responses.append({}) + await mock_controller.async_update() + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" + + client_1_client["is_wired"] = False + client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + mock_controller.mock_client_responses.append([client_1_client]) + mock_controller.mock_device_responses.append({}) + await mock_controller.async_update() + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" + + async def test_restoring_client(hass, mock_controller): """Test the update_items function with some clients.""" mock_controller.mock_client_responses.append([CLIENT_2]) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index e660e57fc671d5..7ea5e0680b9c44 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -17,6 +17,7 @@ CONF_SITE_ID, CONTROLLER_ID as CONF_CONTROLLER_ID, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component @@ -221,7 +222,9 @@ def mock_controller(hass): """Mock a UniFi Controller.""" hass.data[UNIFI_CONFIG] = {} + hass.data[UNIFI_WIRELESS_CLIENTS] = Mock() controller = unifi.UniFiController(hass, None) + controller.wireless_clients = set() controller._site_role = "admin" @@ -326,7 +329,7 @@ async def test_switches(hass, mock_controller): await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 4 switch_1 = hass.states.get("switch.poe_client_1") assert switch_1 is not None From d8c6b281b88f97af3b150012ea3bbe5e8dacbc2f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 2 Oct 2019 22:12:59 +0200 Subject: [PATCH 256/296] deCONZ - Support Symfonisk sound controller with device triggers (#26913) * Device trigger tests shall use the common gateway mock * Follow ebaauws clarification of signals * Fix translations --- .../components/deconz/.translations/en.json | 1 + .../components/deconz/device_trigger.py | 15 ++++- homeassistant/components/deconz/strings.json | 1 + .../components/deconz/test_device_trigger.py | 56 +++---------------- 4 files changed, 24 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index ead71db8c27d89..c00bfca35641c2 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", "remote_button_short_press": "\"{subtype}\" button pressed", "remote_button_short_release": "\"{subtype}\" button released", "remote_button_triple_press": "\"{subtype}\" button triple clicked", diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 77efc78562a924..5339eff055e89f 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -31,6 +31,7 @@ CONF_QUADRUPLE_PRESS = "remote_button_quadruple_press" CONF_QUINTUPLE_PRESS = "remote_button_quintuple_press" CONF_ROTATED = "remote_button_rotated" +CONF_ROTATION_STOPPED = "remote_button_rotation_stopped" CONF_SHAKE = "remote_gyro_activated" CONF_TURN_ON = "turn_on" @@ -75,6 +76,17 @@ (CONF_SHORT_PRESS, CONF_BUTTON_4): 18, } +SYMFONISK_SOUND_CONTROLLER_MODEL = "SYMFONISK Sound Controller" +SYMFONISK_SOUND_CONTROLLER = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, + (CONF_TRIPLE_PRESS, CONF_TURN_ON): 1005, + (CONF_ROTATED, CONF_LEFT): 2001, + (CONF_ROTATION_STOPPED, CONF_LEFT): 2003, + (CONF_ROTATED, CONF_RIGHT): 3001, + (CONF_ROTATION_STOPPED, CONF_RIGHT): 3003, +} + TRADFRI_ON_OFF_SWITCH_MODEL = "TRADFRI on/off switch" TRADFRI_ON_OFF_SWITCH = { (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, @@ -162,6 +174,7 @@ REMOTES = { HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, + SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER, TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH, TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE, TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE, @@ -200,7 +213,7 @@ async def async_attach_trigger(hass, config, action, automation_info): trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - if device.model not in REMOTES and trigger not in REMOTES[device.model]: + if device.model not in REMOTES or trigger not in REMOTES[device.model]: raise InvalidDeviceAutomationConfig trigger = REMOTES[device.model][trigger] diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 00aa463349cf68..db43c022822838 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -63,6 +63,7 @@ "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", "remote_gyro_activated": "Device shaken" }, "trigger_subtype": { diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 6590028d76638a..4677ea8d5a7c77 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -1,30 +1,13 @@ """deCONZ device automation tests.""" -from asynctest import patch +from copy import deepcopy -from homeassistant import config_entries -from homeassistant.components import deconz from homeassistant.components.deconz import device_trigger from tests.common import async_get_device_automations -BRIDGEID = "0123456789" +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_SENSOR = { +SENSORS = { "1": { "config": { "alert": "none", @@ -46,37 +29,14 @@ } } -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG, "sensors": DECONZ_SENSOR} - - -async def setup_deconz(hass, options): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=ENTRY_CONFIG, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=DECONZ_WEB_REQUEST - ): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][BRIDGEID] - async def test_get_triggers(hass): """Test triggers work.""" - gateway = await setup_deconz(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) device_id = gateway.events[0].device_id triggers = await async_get_device_automations(hass, "trigger", device_id) From 65ce3b49c18a4a1887393619271d970645084284 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Oct 2019 22:14:52 +0200 Subject: [PATCH 257/296] Add support for `for` to binary_sensor, light and switch device triggers (#26658) * Add support for `for` to binary_sensor, light and switch device triggers * Add WS API device_automation/trigger/capabilities --- homeassistant/components/automation/device.py | 9 +- .../binary_sensor/device_trigger.py | 15 ++- .../components/deconz/device_trigger.py | 2 - .../components/device_automation/__init__.py | 93 ++++++++++++++---- .../device_automation/toggle_entity.py | 21 +++- .../components/light/device_action.py | 1 - .../components/light/device_trigger.py | 6 +- .../components/switch/device_action.py | 1 - .../components/switch/device_trigger.py | 6 +- homeassistant/components/zha/device_action.py | 1 - .../components/zha/device_trigger.py | 1 - homeassistant/helpers/config_validation.py | 14 ++- homeassistant/helpers/script.py | 11 ++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- tests/common.py | 1 + .../binary_sensor/test_device_trigger.py | 84 ++++++++++++++++ .../components/device_automation/test_init.py | 97 +++++++++++++++++++ tests/components/light/test_device_trigger.py | 84 ++++++++++++++++ .../components/switch/test_device_trigger.py | 84 ++++++++++++++++ 21 files changed, 495 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index eb3e5a95c9c17e..dc65008c3fb268 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -5,6 +5,7 @@ TRIGGER_BASE_SCHEMA, async_get_device_automation_platform, ) +from homeassistant.const import CONF_DOMAIN # mypy: allow-untyped-defs, no-check-untyped-defs @@ -14,11 +15,15 @@ async def async_validate_trigger_config(hass, config): """Validate config.""" - platform = await async_get_device_automation_platform(hass, config, "trigger") + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "trigger" + ) return platform.TRIGGER_SCHEMA(config) async def async_attach_trigger(hass, config, action, automation_info): """Listen for trigger.""" - platform = await async_get_device_automation_platform(hass, config, "trigger") + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "trigger" + ) return await platform.async_attach_trigger(hass, config, action, automation_info) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 2211b3001045ed..c4d2efcb63b2bb 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -7,7 +7,7 @@ CONF_TURNED_OFF, CONF_TURNED_ON, ) -from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import config_validation as cv @@ -175,13 +175,13 @@ { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) trigger_type = config[CONF_TYPE] if trigger_type in TURNED_ON: from_state = "off" @@ -195,6 +195,8 @@ async def async_attach_trigger(hass, config, action, automation_info): state_automation.CONF_FROM: from_state, state_automation.CONF_TO: to_state, } + if "for" in config: + state_config["for"] = config["for"] return await state_automation.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" @@ -236,3 +238,12 @@ async def async_get_triggers(hass, device_id): ) return triggers + + +async def async_get_trigger_capabilities(hass, trigger): + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 5339eff055e89f..badbe8b8651278 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -206,8 +206,6 @@ def _get_deconz_event_from_device_id(hass, device_id): async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - device_registry = await hass.helpers.device_registry.async_get_registry() device = device_registry.async_get(config[CONF_DEVICE_ID]) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 23e320fe153b60..a7e04f874b4d83 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -4,9 +4,11 @@ from typing import Any, List, MutableMapping import voluptuous as vol +import voluptuous_serialize from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID from homeassistant.components import websocket_api +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.loader import async_get_integration, IntegrationNotFound @@ -29,9 +31,18 @@ ) TYPES = { - "trigger": ("device_trigger", "async_get_triggers"), - "condition": ("device_condition", "async_get_conditions"), - "action": ("device_action", "async_get_actions"), + # platform name, get automations function, get capabilities function + "trigger": ( + "device_trigger", + "async_get_triggers", + "async_get_trigger_capabilities", + ), + "condition": ( + "device_condition", + "async_get_conditions", + "async_get_condition_capabilities", + ), + "action": ("device_action", "async_get_actions", "async_get_action_capabilities"), } @@ -46,25 +57,26 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( websocket_device_automation_list_triggers ) + hass.components.websocket_api.async_register_command( + websocket_device_automation_get_trigger_capabilities + ) return True -async def async_get_device_automation_platform(hass, config, automation_type): +async def async_get_device_automation_platform(hass, domain, automation_type): """Load device automation platform for integration. Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. """ - platform_name, _ = TYPES[automation_type] + platform_name = TYPES[automation_type][0] try: - integration = await async_get_integration(hass, config[CONF_DOMAIN]) + integration = await async_get_integration(hass, domain) platform = integration.get_platform(platform_name) except IntegrationNotFound: - raise InvalidDeviceAutomationConfig( - f"Integration '{config[CONF_DOMAIN]}' not found" - ) + raise InvalidDeviceAutomationConfig(f"Integration '{domain}' not found") except ImportError: raise InvalidDeviceAutomationConfig( - f"Integration '{config[CONF_DOMAIN]}' does not support device automation {automation_type}s" + f"Integration '{domain}' does not support device automation {automation_type}s" ) return platform @@ -74,20 +86,14 @@ async def _async_get_device_automations_from_domain( hass, domain, automation_type, device_id ): """List device automations.""" - integration = None try: - integration = await async_get_integration(hass, domain) - except IntegrationNotFound: - _LOGGER.warning("Integration %s not found", domain) + platform = await async_get_device_automation_platform( + hass, domain, automation_type + ) + except InvalidDeviceAutomationConfig: return None - platform_name, function_name = TYPES[automation_type] - - try: - platform = integration.get_platform(platform_name) - except ImportError: - # The domain does not have device automations - return None + function_name = TYPES[automation_type][1] return await getattr(platform, function_name)(hass, device_id) @@ -125,6 +131,35 @@ async def _async_get_device_automations(hass, automation_type, device_id): return automations +async def _async_get_device_automation_capabilities(hass, automation_type, automation): + """List device automations.""" + try: + platform = await async_get_device_automation_platform( + hass, automation[CONF_DOMAIN], automation_type + ) + except InvalidDeviceAutomationConfig: + return {} + + function_name = TYPES[automation_type][2] + + if not hasattr(platform, function_name): + # The device automation has no capabilities + return {} + + capabilities = await getattr(platform, function_name)(hass, automation) + capabilities = capabilities.copy() + + extra_fields = capabilities.get("extra_fields") + if extra_fields is None: + capabilities["extra_fields"] = [] + else: + capabilities["extra_fields"] = voluptuous_serialize.convert( + extra_fields, custom_serializer=cv.custom_serializer + ) + + return capabilities + + @websocket_api.async_response @websocket_api.websocket_command( { @@ -165,3 +200,19 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): device_id = msg["device_id"] triggers = await _async_get_device_automations(hass, "trigger", device_id) connection.send_result(msg["id"], triggers) + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/trigger/capabilities", + vol.Required("trigger"): dict, + } +) +async def websocket_device_automation_get_trigger_capabilities(hass, connection, msg): + """Handle request for device trigger capabilities.""" + trigger = msg["trigger"] + capabilities = await _async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + connection.send_result(msg["id"], capabilities) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index ef1b605f4d68e0..7c68be83ba30ec 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -13,7 +13,13 @@ CONF_TURNED_OFF, CONF_TURNED_ON, ) -from homeassistant.const import CONF_CONDITION, CONF_ENTITY_ID, CONF_PLATFORM, CONF_TYPE +from homeassistant.const import ( + CONF_CONDITION, + CONF_ENTITY_ID, + CONF_FOR, + CONF_PLATFORM, + CONF_TYPE, +) from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import condition, config_validation as cv, service from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -81,6 +87,7 @@ { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) @@ -93,7 +100,6 @@ async def async_call_action_from_config( domain: str, ) -> None: """Change state based on configuration.""" - config = ACTION_SCHEMA(config) action_type = config[CONF_TYPE] if action_type == CONF_TURN_ON: action = "turn_on" @@ -149,6 +155,8 @@ async def async_attach_trigger( state.CONF_FROM: from_state, state.CONF_TO: to_state, } + if "for" in config: + state_config["for"] = config["for"] return await state.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" @@ -203,3 +211,12 @@ async def async_get_triggers( ) -> List[dict]: """List device triggers.""" return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index ea37b8e9470534..9d8ef6bceafb32 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -19,7 +19,6 @@ async def async_call_action_from_config( context: Context, ) -> None: """Change state based on configuration.""" - config = ACTION_SCHEMA(config) await toggle_entity.async_call_action_from_config( hass, config, variables, context, DOMAIN ) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index f2a82afdc2d41c..5bd5d83e1c02ee 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -22,7 +22,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) return await toggle_entity.async_attach_trigger( hass, config, action, automation_info ) @@ -31,3 +30,8 @@ async def async_attach_trigger( async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: """List device triggers.""" return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: + """List trigger capabilities.""" + return await toggle_entity.async_get_trigger_capabilities(hass, trigger) diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py index ca91cc70512be5..a65c1acc5124b8 100644 --- a/homeassistant/components/switch/device_action.py +++ b/homeassistant/components/switch/device_action.py @@ -19,7 +19,6 @@ async def async_call_action_from_config( context: Context, ) -> None: """Change state based on configuration.""" - config = ACTION_SCHEMA(config) await toggle_entity.async_call_action_from_config( hass, config, variables, context, DOMAIN ) diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 9be294d5460c99..22a016e49b9bc2 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -22,7 +22,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) return await toggle_entity.async_attach_trigger( hass, config, action, automation_info ) @@ -31,3 +30,8 @@ async def async_attach_trigger( async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: """List device triggers.""" return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: + """List trigger capabilities.""" + return await toggle_entity.async_get_trigger_capabilities(hass, trigger) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 27e78507bfb317..460676a75a0045 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -49,7 +49,6 @@ async def async_call_action_from_config( context: Context, ) -> None: """Perform an action based on configuration.""" - config = ACTION_SCHEMA(config) await ZHA_ACTION_TYPES[DEVICE_ACTION_TYPES[config[CONF_TYPE]]]( hass, config, variables, context ) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 331dc3d32968d3..c1ea3c2b761698 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -23,7 +23,6 @@ async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index d0aeb4f4968bc6..2d1bb89d23a56e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -15,8 +15,9 @@ from urllib.parse import urlparse from uuid import UUID -import voluptuous as vol from pkg_resources import parse_version +import voluptuous as vol +import voluptuous_serialize import homeassistant.util.dt as dt_util from homeassistant.const import ( @@ -374,6 +375,9 @@ def positive_timedelta(value: timedelta) -> timedelta: return value +positive_time_period_dict = vol.All(time_period_dict, positive_timedelta) + + def remove_falsy(value: List[T]) -> List[T]: """Remove falsy values from a list.""" return [v for v in value if v] @@ -690,6 +694,14 @@ def validator(value): return validator +def custom_serializer(schema): + """Serialize additional types for voluptuous_serialize.""" + if schema is positive_time_period_dict: + return {"type": "positive_time_period_dict"} + + return voluptuous_serialize.UNSUPPORTED + + # Schemas PLATFORM_SCHEMA = vol.Schema( { diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index e383f1013ab204..d9b3df8c01b717 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -10,7 +10,12 @@ import homeassistant.components.device_automation as device_automation from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE -from homeassistant.const import CONF_CONDITION, CONF_DEVICE_ID, CONF_TIMEOUT +from homeassistant.const import ( + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_TIMEOUT, +) from homeassistant import exceptions from homeassistant.helpers import ( service, @@ -89,7 +94,7 @@ async def async_validate_action_config( if action_type == ACTION_DEVICE_AUTOMATION: platform = await device_automation.async_get_device_automation_platform( - hass, config, "action" + hass, config[CONF_DOMAIN], "action" ) config = platform.ACTION_SCHEMA(config) @@ -346,7 +351,7 @@ async def _async_device_automation(self, action, variables, context): self.last_action = action.get(CONF_ALIAS, "device automation") self._log("Executing step %s" % self.last_action) platform = await device_automation.async_get_device_automation_platform( - self.hass, action, "action" + self.hass, action[CONF_DOMAIN], "action" ) await platform.async_call_action_from_config( self.hass, action, variables, context diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 684285a1cf10b3..c500ddca85d4c5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 sqlalchemy==1.3.8 -voluptuous-serialize==2.2.0 +voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index a0f64eeec74b9d..4a2a2cf45fcf76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -17,7 +17,7 @@ pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 voluptuous==0.11.7 -voluptuous-serialize==2.2.0 +voluptuous-serialize==2.3.0 # homeassistant.components.nuimo_controller --only-binary=all nuimo==0.1.0 diff --git a/setup.py b/setup.py index d842ae39ae1b77..23a8a808f4376f 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ "requests==2.22.0", "ruamel.yaml==0.15.100", "voluptuous==0.11.7", - "voluptuous-serialize==2.2.0", + "voluptuous-serialize==2.3.0", ] MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) diff --git a/tests/common.py b/tests/common.py index 1982e80dfe949e..bd611e04c37a6e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -56,6 +56,7 @@ from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.components.device_automation import ( # noqa _async_get_device_automations as async_get_device_automations, + _async_get_device_automation_capabilities as async_get_device_automation_capabilities, ) _TEST_INSTANCE_PORT = SERVER_PORT diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 5be354c78fc5cf..9bab1ff1f363e9 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -1,4 +1,5 @@ """The test for binary_sensor device automation.""" +from datetime import timedelta import pytest from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES @@ -7,13 +8,16 @@ from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, + async_fire_time_changed, async_mock_service, mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -71,6 +75,28 @@ async def test_get_triggers(hass, device_reg, entity_reg): assert triggers == expected_triggers +async def test_get_trigger_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a binary_sensor trigger.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + async def test_if_fires_on_state_change(hass, calls): """Test for on and off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -152,3 +178,61 @@ async def test_if_fires_on_state_change(hass, calls): assert calls[1].data["some"] == "bat_low device - {} - off - on - None".format( sensor1.entity_id ) + + +async def test_if_fires_on_state_change_with_for(hass, calls): + """Test for triggers firing with delay.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "turned_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + await hass.async_block_till_done() + assert calls[0].data["some"] == "turn_off device - {} - on - off - 0:00:05".format( + sensor1.entity_id + ) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index acfa853d596f27..8a92f69e57493a 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -164,6 +164,103 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert _same_lists(triggers, expected_triggers) +async def test_websocket_get_trigger_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get the expected trigger capabilities for a light through websocket.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/trigger/list", + "device_id": device_entry.id, + } + ) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + triggers = msg["result"] + + id = 2 + for trigger in triggers: + await client.send_json( + { + "id": id, + "type": "device_automation/trigger/capabilities", + "trigger": trigger, + } + ) + msg = await client.receive_json() + assert msg["id"] == id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + id = id + 1 + + +async def test_websocket_get_bad_trigger_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no trigger capabilities for a non existing domain.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/trigger/capabilities", + "trigger": {"domain": "beer"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + +async def test_websocket_get_no_trigger_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no trigger capabilities for a domain with no device trigger capabilities.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/trigger/capabilities", + "trigger": {"domain": "deconz"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + async def test_automation_with_non_existing_integration(hass, caplog): """Test device automation with non existing integration.""" assert await async_setup_component( diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index 9b540c7aa15af7..a6437ef9ee0a64 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -1,4 +1,5 @@ """The test for light device automation.""" +from datetime import timedelta import pytest from homeassistant.components.light import DOMAIN @@ -6,13 +7,16 @@ from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, + async_fire_time_changed, async_mock_service, mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -63,6 +67,28 @@ async def test_get_triggers(hass, device_reg, entity_reg): assert triggers == expected_triggers +async def test_get_trigger_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a light trigger.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + async def test_if_fires_on_state_change(hass, calls): """Test for turn_on and turn_off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -145,3 +171,61 @@ async def test_if_fires_on_state_change(hass, calls): assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( ent1.entity_id ) + + +async def test_if_fires_on_state_change_with_for(hass, calls): + """Test for triggers firing with delay.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + await hass.async_block_till_done() + assert calls[0].data["some"] == "turn_off device - {} - on - off - 0:00:05".format( + ent1.entity_id + ) diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 43af9fe3df34b2..31fb6d30f60994 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -1,4 +1,5 @@ """The test for switch device automation.""" +from datetime import timedelta import pytest from homeassistant.components.switch import DOMAIN @@ -6,13 +7,16 @@ from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, + async_fire_time_changed, async_mock_service, mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -63,6 +67,28 @@ async def test_get_triggers(hass, device_reg, entity_reg): assert triggers == expected_triggers +async def test_get_trigger_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a switch trigger.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + async def test_if_fires_on_state_change(hass, calls): """Test for turn_on and turn_off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -145,3 +171,61 @@ async def test_if_fires_on_state_change(hass, calls): assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( ent1.entity_id ) + + +async def test_if_fires_on_state_change_with_for(hass, calls): + """Test for triggers firing with delay.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + await hass.async_block_till_done() + assert calls[0].data["some"] == "turn_off device - {} - on - off - 0:00:05".format( + ent1.entity_id + ) From 743cb848e883062b55a6e897ecb5842598058644 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 3 Oct 2019 00:08:01 +0200 Subject: [PATCH 258/296] Updated frontend to 20191002.0 (#27134) --- homeassistant/components/frontend/manifest.json | 10 +++++++--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f1d91879f155eb..60a4f0faa9c16f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20190919.1"], + "requirements": [ + "home-assistant-frontend==20191002.0" + ], "dependencies": [ "api", "auth", @@ -12,5 +14,7 @@ "system_log", "websocket_api" ], - "codeowners": ["@home-assistant/frontend"] -} + "codeowners": [ + "@home-assistant/frontend" + ] +} \ No newline at end of file diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c500ddca85d4c5..29484b671ed0e1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190919.1 +home-assistant-frontend==20191002.0 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4a2a2cf45fcf76..2aa04f8ae1551e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -640,7 +640,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190919.1 +home-assistant-frontend==20191002.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb29540d6d93c7..61d3479d8f62d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -182,7 +182,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190919.1 +home-assistant-frontend==20191002.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 30245f68741986f951f19cd5c8283cb6e15cfdeb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 2 Oct 2019 17:51:18 -0500 Subject: [PATCH 259/296] Fix error on failed Plex setup (#27132) --- homeassistant/components/plex/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 874ac6334acf5b..ed94b6913bcf57 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -121,7 +121,7 @@ async def async_setup_entry(hass, entry): ) as error: _LOGGER.error( "Login to %s failed, verify token and SSL settings: [%s]", - server_config[CONF_SERVER], + entry.data[CONF_SERVER], error, ) return False From e011a94ce9024ab29774c5b5f49b20ba727eb26f Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 2 Oct 2019 18:51:52 -0400 Subject: [PATCH 260/296] Bump up ZHA dependencies. (#27127) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index ab8a20822a1365..59d9508ac338d8 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "bellows-homeassistant==0.10.0", "zha-quirks==0.0.26", - "zigpy-deconz==0.4.0", + "zigpy-deconz==0.5.0", "zigpy-homeassistant==0.9.0", "zigpy-xbee-homeassistant==0.5.0", "zigpy-zigate==0.4.0" diff --git a/requirements_all.txt b/requirements_all.txt index 2aa04f8ae1551e..cda5afabe7cb8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2032,7 +2032,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.4.0 +zigpy-deconz==0.5.0 # homeassistant.components.zha zigpy-homeassistant==0.9.0 From 6dfeed6cd1aacf27bb8cb588965057bc58a0d447 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 2 Oct 2019 18:53:04 -0400 Subject: [PATCH 261/296] Fix unavailable climate entities in Alexa StateReport (#27128) * Return None for AlexaThermostatController and AlexaTemperatureSensor properties if climate state is unavailable. Preserves raising an error for UnsupportedProperty, and allows Alexa.EndpointHealth to handle the unavailable state. * Added additional tests for climate state reporting. --- .../components/alexa/capabilities.py | 5 +- tests/components/alexa/test_capabilities.py | 109 +++++++++++++++++- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index aeaa0a62c4bdbb..c8bc76fbe83edb 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -445,7 +445,7 @@ def get_property(self, name): unit = self.hass.config.units.temperature_unit temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE) - if temp in (STATE_UNAVAILABLE, STATE_UNKNOWN): + if temp in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): return None try: @@ -572,6 +572,9 @@ def properties_retrievable(self): def get_property(self, name): """Read and return a property.""" + if self.entity.state == STATE_UNAVAILABLE: + return None + if name == "thermostatMode": preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 357e0e3026d47c..94c931e351405f 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -9,8 +9,9 @@ STATE_UNKNOWN, STATE_UNAVAILABLE, ) -from homeassistant.components import climate +from homeassistant.components.climate import const as climate from homeassistant.components.alexa import smart_home +from homeassistant.components.alexa.errors import UnsupportedProperty from tests.common import async_mock_service from . import ( @@ -378,6 +379,112 @@ async def test_report_cover_percentage_state(hass): properties.assert_equal("Alexa.PercentageController", "percentage", 0) +async def test_report_climate_state(hass): + """Test ThermostatController reports state correctly.""" + for auto_modes in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): + hass.states.async_set( + "climate.downstairs", + auto_modes, + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "AUTO") + properties.assert_equal( + "Alexa.TemperatureSensor", + "temperature", + {"value": 34.0, "scale": "CELSIUS"}, + ) + + for off_modes in ( + climate.HVAC_MODE_OFF, + climate.HVAC_MODE_FAN_ONLY, + climate.HVAC_MODE_DRY, + ): + hass.states.async_set( + "climate.downstairs", + off_modes, + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "OFF") + properties.assert_equal( + "Alexa.TemperatureSensor", + "temperature", + {"value": 34.0, "scale": "CELSIUS"}, + ) + + hass.states.async_set( + "climate.heat", + "heat", + { + "friendly_name": "Climate Heat", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.heat") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "HEAT") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) + + hass.states.async_set( + "climate.cool", + "cool", + { + "friendly_name": "Climate Cool", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.cool") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "COOL") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) + + hass.states.async_set( + "climate.unavailable", + "unavailable", + {"friendly_name": "Climate Unavailable", "supported_features": 91}, + ) + properties = await reported_properties(hass, "climate.unavailable") + properties.assert_not_has_property("Alexa.ThermostatController", "thermostatMode") + + hass.states.async_set( + "climate.unsupported", + "blablabla", + { + "friendly_name": "Climate Unsupported", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + with pytest.raises(UnsupportedProperty): + properties = await reported_properties(hass, "climate.unsupported") + properties.assert_not_has_property( + "Alexa.ThermostatController", "thermostatMode" + ) + properties.assert_equal( + "Alexa.TemperatureSensor", + "temperature", + {"value": 34.0, "scale": "CELSIUS"}, + ) + + async def test_temperature_sensor_sensor(hass): """Test TemperatureSensor reports sensor temperature correctly.""" for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"): From 39c7d069b8d264353ce7ebe63a09cfec54224a3a Mon Sep 17 00:00:00 2001 From: Brendon Baumgartner Date: Wed, 2 Oct 2019 15:53:37 -0700 Subject: [PATCH 262/296] gpiozero requirement ver (#27129) --- homeassistant/components/remote_rpi_gpio/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/remote_rpi_gpio/manifest.json b/homeassistant/components/remote_rpi_gpio/manifest.json index df9a9c7512358c..c3b93346916a5f 100644 --- a/homeassistant/components/remote_rpi_gpio/manifest.json +++ b/homeassistant/components/remote_rpi_gpio/manifest.json @@ -3,7 +3,7 @@ "name": "remote_rpi_gpio", "documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio", "requirements": [ - "gpiozero==1.4.1" + "gpiozero==1.5.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index cda5afabe7cb8f..4f0963cdbef6dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -574,7 +574,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.remote_rpi_gpio -gpiozero==1.4.1 +gpiozero==1.5.1 # homeassistant.components.gpsd gps3==0.33.3 From 75bce84ad5c4bbc4a9a3de798a6e6753ae982926 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 3 Oct 2019 00:53:55 +0200 Subject: [PATCH 263/296] Update KNX integration to xknx 0.11.2 (#27130) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 76f15f3bdb8efd..f99ec2f22c01fd 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "Knx", "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ - "xknx==0.11.1" + "xknx==0.11.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4f0963cdbef6dc..c84b57a09f36b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1986,7 +1986,7 @@ xboxapi==0.1.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.11.1 +xknx==0.11.2 # homeassistant.components.bluesound # homeassistant.components.startca From 363873dfcba399d45f1ee618cb294044c765eef5 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 2 Oct 2019 18:55:01 -0400 Subject: [PATCH 264/296] Display Fan entity as Fan category in Alexa (#27135) * Added Fan to display categories. * Added Doorbell to display categories. * Added Microwave to display categories. * Added Security Panel to display categories. * Updated FanCapabilities to use FAN display category. * Updated Tests for FanCapabilities to use FAN display category. --- homeassistant/components/alexa/entities.py | 14 +++++++++++++- tests/components/alexa/test_smart_home.py | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index f0d72af23d50f4..55b5878f6672a3 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -76,9 +76,18 @@ class DisplayCategory: # Indicates a door. DOOR = "DOOR" + # Indicates a doorbell. + DOOR_BELL = "DOORBELL" + + # Indicates a fan. + FAN = "FAN" + # Indicates light sources or fixtures. LIGHT = "LIGHT" + # Indicates a microwave oven. + MICROWAVE = "MICROWAVE" + # Indicates an endpoint that detects and reports motion. MOTION_SENSOR = "MOTION_SENSOR" @@ -91,6 +100,9 @@ class DisplayCategory: # order is unimportant. Applies to Scenes SCENE_TRIGGER = "SCENE_TRIGGER" + # Indicates a security panel. + SECURITY_PANEL = "SECURITY_PANEL" + # Indicates an endpoint that locks. SMARTLOCK = "SMARTLOCK" @@ -324,7 +336,7 @@ class FanCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" - return [DisplayCategory.OTHER] + return [DisplayCategory.FAN] def interfaces(self): """Yield the supported interfaces.""" diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3cafa89902471f..e5e5b8ab7aecf0 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -308,7 +308,7 @@ async def test_fan(hass): appliance = await discovery_test(device, hass) assert appliance["endpointId"] == "fan#test_1" - assert appliance["displayCategories"][0] == "OTHER" + assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 1" assert_endpoint_capabilities( appliance, "Alexa.PowerController", "Alexa.EndpointHealth" @@ -333,7 +333,7 @@ async def test_variable_fan(hass): appliance = await discovery_test(device, hass) assert appliance["endpointId"] == "fan#test_2" - assert appliance["displayCategories"][0] == "OTHER" + assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 2" assert_endpoint_capabilities( From c43eeee62f2187000b1abe1506e1c6025f0fcad0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 00:58:14 +0200 Subject: [PATCH 265/296] Improve validation of device condition config (#27131) * Improve validation of device condition config * Fix typing --- homeassistant/components/automation/config.py | 11 +- .../binary_sensor/device_condition.py | 3 +- .../components/device_automation/__init__.py | 6 +- .../components/light/device_condition.py | 3 +- .../components/switch/device_condition.py | 3 +- homeassistant/helpers/condition.py | 51 +++-- homeassistant/helpers/script.py | 7 +- .../components/device_automation/test_init.py | 206 +++++++++++++++++- 8 files changed, 269 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 3f48e2afde67f8..581ce6b461d844 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -7,10 +7,10 @@ from homeassistant.const import CONF_PLATFORM from homeassistant.config import async_log_exception, config_without_domain from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform, script +from homeassistant.helpers import condition, config_per_platform, script from homeassistant.loader import IntegrationNotFound -from . import CONF_ACTION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA +from . import CONF_ACTION, CONF_CONDITION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -33,6 +33,13 @@ async def async_validate_config_item(hass, config, full_config=None): triggers.append(trigger) config[CONF_TRIGGER] = triggers + if CONF_CONDITION in config: + conditions = [] + for cond in config[CONF_CONDITION]: + cond = await condition.async_validate_condition_config(hass, cond) + conditions.append(cond) + config[CONF_CONDITION] = conditions + actions = [] for action in config[CONF_ACTION]: action = await script.async_validate_action_config(hass, action) diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 70b79becb8b12d..1749ea91c5b552 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -232,7 +232,8 @@ def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) + if config_validation: + config = CONDITION_SCHEMA(config) condition_type = config[CONF_TYPE] if condition_type in IS_ON: stat = "on" diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index a7e04f874b4d83..fa6deac40ba6ac 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -2,12 +2,14 @@ import asyncio import logging from typing import Any, List, MutableMapping +from types import ModuleType import voluptuous as vol import voluptuous_serialize from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.loader import async_get_integration, IntegrationNotFound @@ -63,7 +65,9 @@ async def async_setup(hass, config): return True -async def async_get_device_automation_platform(hass, domain, automation_type): +async def async_get_device_automation_platform( + hass: HomeAssistant, domain: str, automation_type: str +) -> ModuleType: """Load device automation platform for integration. Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index a69ca7ab8f2f2f..4abf34e6661bac 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -19,7 +19,8 @@ def async_condition_from_config( config: ConfigType, config_validation: bool ) -> ConditionCheckerType: """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) + if config_validation: + config = CONDITION_SCHEMA(config) return toggle_entity.async_condition_from_config(config, config_validation) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 032c765bf5907c..5825a3ba91ad4f 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -19,7 +19,8 @@ def async_condition_from_config( config: ConfigType, config_validation: bool ) -> ConditionCheckerType: """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) + if config_validation: + config = CONDITION_SCHEMA(config) return toggle_entity.async_condition_from_config(config, config_validation) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index afb8c3934a7442..df82ba6076fd83 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -8,29 +8,31 @@ from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from homeassistant.loader import async_get_integration from homeassistant.core import HomeAssistant, State from homeassistant.components import zone as zone_cmp +from homeassistant.components.device_automation import ( + async_get_device_automation_platform, +) from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, + CONF_ABOVE, + CONF_AFTER, + CONF_BEFORE, + CONF_BELOW, + CONF_CONDITION, CONF_DOMAIN, CONF_ENTITY_ID, - CONF_VALUE_TEMPLATE, - CONF_CONDITION, - WEEKDAYS, CONF_STATE, - CONF_ZONE, - CONF_BEFORE, - CONF_AFTER, + CONF_VALUE_TEMPLATE, CONF_WEEKDAY, - SUN_EVENT_SUNRISE, - SUN_EVENT_SUNSET, - CONF_BELOW, - CONF_ABOVE, + CONF_ZONE, STATE_UNAVAILABLE, STATE_UNKNOWN, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, + WEEKDAYS, ) from homeassistant.exceptions import TemplateError, HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -498,9 +500,32 @@ async def async_device_from_config( """Test a device condition.""" if config_validation: config = cv.DEVICE_CONDITION_SCHEMA(config) - integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_condition") + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "condition" + ) return cast( ConditionCheckerType, platform.async_condition_from_config(config, config_validation), # type: ignore ) + + +async def async_validate_condition_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + condition = config[CONF_CONDITION] + if condition in ("and", "or"): + conditions = [] + for sub_cond in config["conditions"]: + sub_cond = await async_validate_condition_config(hass, sub_cond) + conditions.append(sub_cond) + config["conditions"] = conditions + + if condition == "device": + config = cv.DEVICE_CONDITION_SCHEMA(config) + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "condition" + ) + return cast(ConfigType, platform.CONDITION_SCHEMA(config)) # type: ignore + + return config diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d9b3df8c01b717..05b281027262ff 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -96,7 +96,12 @@ async def async_validate_action_config( platform = await device_automation.async_get_device_automation_platform( hass, config[CONF_DOMAIN], "action" ) - config = platform.ACTION_SCHEMA(config) + config = platform.ACTION_SCHEMA(config) # type: ignore + if action_type == ACTION_CHECK_CONDITION and config[CONF_CONDITION] == "device": + platform = await device_automation.async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "condition" + ) + config = platform.CONDITION_SCHEMA(config) # type: ignore return config diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 8a92f69e57493a..fa78ae94416361 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -4,10 +4,16 @@ from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM from homeassistant.helpers import device_registry -from tests.common import MockConfigEntry, mock_device_registry, mock_registry +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, +) @pytest.fixture @@ -301,6 +307,31 @@ async def test_automation_with_integration_without_device_action(hass, caplog): ) +async def test_automation_with_integration_without_device_condition(hass, caplog): + """Test automation with integration without device condition support.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "device_id": "none", + "domain": "test", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert ( + "Integration 'test' does not support device automation conditions" + in caplog.text + ) + + async def test_automation_with_integration_without_device_trigger(hass, caplog): """Test automation with integration without device trigger support.""" assert await async_setup_component( @@ -341,6 +372,179 @@ async def test_automation_with_bad_action(hass, caplog): assert "required key not provided" in caplog.text +async def test_automation_with_bad_condition_action(hass, caplog): + """Test automation with bad device action.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": {"condition": "device", "device_id": "", "domain": "light"}, + } + }, + ) + + assert "required key not provided" in caplog.text + + +async def test_automation_with_bad_condition(hass, caplog): + """Test automation with bad device condition.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": {"condition": "device", "domain": "light"}, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "required key not provided" in caplog.text + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_automation_with_sub_condition(hass, calls): + """Test automation with device condition under and/or conditions.""" + DOMAIN = "light" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "and", + "conditions": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_on", + }, + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent2.entity_id, + "type": "is_on", + }, + ], + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "and {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "or", + "conditions": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_on", + }, + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent2.entity_id, + "type": "is_on", + }, + ], + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "or {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert hass.states.get(ent2.entity_id).state == STATE_OFF + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "or event - test_event1" + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.states.async_set(ent2.entity_id, STATE_ON) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "or event - test_event1" + + hass.states.async_set(ent1.entity_id, STATE_ON) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 4 + assert _same_lists( + [calls[2].data["some"], calls[3].data["some"]], + ["or event - test_event1", "and event - test_event1"], + ) + + +async def test_automation_with_bad_sub_condition(hass, caplog): + """Test automation with bad device condition under and/or conditions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "and", + "conditions": [{"condition": "device", "domain": "light"}], + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "required key not provided" in caplog.text + + async def test_automation_with_bad_trigger(hass, caplog): """Test automation with bad device trigger.""" assert await async_setup_component( From 9c1feacd47671eb4935215c31110b778f3a63eb5 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 2 Oct 2019 18:59:21 -0400 Subject: [PATCH 266/296] Fix colorTemperatureInKelvin in Alexa report when light is off (#27107) * Fixes #26405 Return None if light state is off since attribute is unavailable, prevents property from being reported with invalid value of 0. * Update Test to check property is not reported when light state is off. --- homeassistant/components/alexa/capabilities.py | 2 +- tests/components/alexa/test_capabilities.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index c8bc76fbe83edb..b8bd3841a78c4c 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -326,7 +326,7 @@ def get_property(self, name): return color_util.color_temperature_mired_to_kelvin( self.entity.attributes["color_temp"] ) - return 0 + return None class AlexaPercentageController(AlexaCapibility): diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 94c931e351405f..d53f145e6ff7f6 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -294,8 +294,8 @@ async def test_report_colored_temp_light_state(hass): ) properties = await reported_properties(hass, "light.test_off") - properties.assert_equal( - "Alexa.ColorTemperatureController", "colorTemperatureInKelvin", 0 + properties.assert_not_has_property( + "Alexa.ColorTemperatureController", "colorTemperatureInKelvin" ) From e005f6f23a3ff9ff052afd317891da206ea618e0 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 3 Oct 2019 00:34:28 +0000 Subject: [PATCH 267/296] [ci skip] Translation update --- .../ambiclimate/.translations/en.json | 2 +- .../arcam_fmj/.translations/es-419.json | 5 ++ .../binary_sensor/.translations/es-419.json | 65 +++++++++++++++++++ .../cert_expiry/.translations/es-419.json | 5 ++ .../deconz/.translations/es-419.json | 36 +++++++++- .../components/heos/.translations/es-419.json | 5 ++ .../.translations/es-419.json | 1 + .../iaqualink/.translations/es-419.json | 13 ++++ .../life360/.translations/es-419.json | 18 +++++ .../light/.translations/es-419.json | 12 ++++ .../linky/.translations/es-419.json | 25 +++++++ .../logi_circle/.translations/en.json | 2 +- .../components/nest/.translations/en.json | 2 +- .../components/nest/.translations/es-419.json | 3 +- .../notion/.translations/es-419.json | 1 + .../components/plex/.translations/da.json | 1 + .../components/plex/.translations/es-419.json | 55 ++++++++++++++++ .../components/plex/.translations/no.json | 3 +- .../components/point/.translations/en.json | 2 +- .../components/ps4/.translations/en.json | 6 +- .../components/soma/.translations/da.json | 1 + .../somfy/.translations/es-419.json | 5 ++ .../switch/.translations/es-419.json | 18 +++++ .../tellduslive/.translations/es-419.json | 4 +- .../components/toon/.translations/en.json | 2 +- .../components/toon/.translations/es-419.json | 14 +++- .../traccar/.translations/es-419.json | 8 +++ .../twentemilieu/.translations/es-419.json | 10 +++ .../velbus/.translations/es-419.json | 21 ++++++ .../vesync/.translations/es-419.json | 20 ++++++ .../withings/.translations/es-419.json | 19 ++++++ .../components/zha/.translations/es-419.json | 18 +++++ .../components/zha/.translations/ru.json | 5 ++ 33 files changed, 392 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/es-419.json create mode 100644 homeassistant/components/binary_sensor/.translations/es-419.json create mode 100644 homeassistant/components/cert_expiry/.translations/es-419.json create mode 100644 homeassistant/components/iaqualink/.translations/es-419.json create mode 100644 homeassistant/components/light/.translations/es-419.json create mode 100644 homeassistant/components/linky/.translations/es-419.json create mode 100644 homeassistant/components/plex/.translations/es-419.json create mode 100644 homeassistant/components/somfy/.translations/es-419.json create mode 100644 homeassistant/components/switch/.translations/es-419.json create mode 100644 homeassistant/components/traccar/.translations/es-419.json create mode 100644 homeassistant/components/twentemilieu/.translations/es-419.json create mode 100644 homeassistant/components/velbus/.translations/es-419.json create mode 100644 homeassistant/components/vesync/.translations/es-419.json create mode 100644 homeassistant/components/withings/.translations/es-419.json diff --git a/homeassistant/components/ambiclimate/.translations/en.json b/homeassistant/components/ambiclimate/.translations/en.json index 32b4a7e2b249c6..da1e173b4a816b 100644 --- a/homeassistant/components/ambiclimate/.translations/en.json +++ b/homeassistant/components/ambiclimate/.translations/en.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Unknown error generating an access token.", "already_setup": "The Ambiclimate account is configured.", - "no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/ambiclimate/)." + "no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Successfully authenticated with Ambiclimate" diff --git a/homeassistant/components/arcam_fmj/.translations/es-419.json b/homeassistant/components/arcam_fmj/.translations/es-419.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/es-419.json b/homeassistant/components/binary_sensor/.translations/es-419.json new file mode 100644 index 00000000000000..f1c20e5346b510 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/es-419.json @@ -0,0 +1,65 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} la bater\u00eda est\u00e1 baja", + "is_cold": "{entity_name} est\u00e1 fr\u00edo", + "is_connected": "{entity_name} est\u00e1 conectado", + "is_gas": "{entity_name} est\u00e1 detectando gas", + "is_hot": "{entity_name} est\u00e1 caliente", + "is_light": "{entity_name} est\u00e1 detectando luz", + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_moist": "{entity_name} est\u00e1 h\u00famedo", + "is_motion": "{entity_name} est\u00e1 detectando movimiento", + "is_moving": "{entity_name} se est\u00e1 moviendo", + "is_no_gas": "{entity_name} no detecta gas", + "is_no_light": "{entity_name} no detecta luz", + "is_no_motion": "{entity_name} no detecta movimiento", + "is_no_problem": "{entity_name} no detecta el problema", + "is_no_smoke": "{entity_name} no detecta humo", + "is_no_sound": "{entity_name} no detecta sonido", + "is_no_vibration": "{entity_name} no detecta vibraciones", + "is_not_bat_low": "{entity_name} bater\u00eda est\u00e1 normal", + "is_not_cold": "{entity_name} no est\u00e1 fr\u00edo", + "is_not_connected": "{entity_name} est\u00e1 desconectado", + "is_not_hot": "{entity_name} no est\u00e1 caliente", + "is_not_locked": "{entity_name} est\u00e1 desbloqueado", + "is_not_moist": "{entity_name} est\u00e1 seco", + "is_not_moving": "{entity_name} no se mueve", + "is_not_occupied": "{entity_name} no est\u00e1 ocupado", + "is_not_open": "{entity_name} est\u00e1 cerrado", + "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_powered": "{entity_name} est\u00e1 encendido", + "is_present": "{entity_name} est\u00e1 presente", + "is_problem": "{entity_name} est\u00e1 detectando un problema", + "is_smoke": "{entity_name} est\u00e1 detectando humo", + "is_sound": "{entity_name} est\u00e1 detectando sonido", + "is_unsafe": "{entity_name} es inseguro", + "is_vibration": "{entity_name} est\u00e1 detectando vibraciones" + }, + "trigger_type": { + "bat_low": "{entity_name} bater\u00eda baja", + "closed": "{entity_name} cerrado", + "cold": "{entity_name} se enfri\u00f3", + "connected": "{entity_name} conectado", + "gas": "{entity_name} comenz\u00f3 a detectar gas", + "hot": "{entity_name} se calent\u00f3", + "light": "{entity_name} comenz\u00f3 a detectar luz", + "locked": "{entity_name} bloqueado", + "moist\u00a7": "{entity_name} se humedeci\u00f3", + "motion": "{entity_name} comenz\u00f3 a detectar movimiento", + "moving": "{entity_name} comenz\u00f3 a moverse", + "no_gas": "{entity_name} dej\u00f3 de detectar gas", + "no_light": "{entity_name} dej\u00f3 de detectar luz", + "no_motion": "{entity_name} dej\u00f3 de detectar movimiento", + "no_problem": "{entity_name} dej\u00f3 de detectar problemas", + "no_smoke": "{entity_name} dej\u00f3 de detectar humo", + "no_sound": "{entity_name} dej\u00f3 de detectar sonido", + "no_vibration": "{entity_name} dej\u00f3 de detectar vibraciones", + "not_bat_low": "{entity_name} bater\u00eda normal", + "not_cold": "{entity_name} no se enfri\u00f3", + "not_connected": "{entity_name} desconectado", + "not_hot": "{entity_name} no se calent\u00f3", + "not_locked": "{entity_name} desbloqueado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/es-419.json b/homeassistant/components/cert_expiry/.translations/es-419.json new file mode 100644 index 00000000000000..392dbf35f5ae13 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Expiraci\u00f3n del certificado" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/es-419.json b/homeassistant/components/deconz/.translations/es-419.json index 1a5d992ef7b914..448b654c86e42d 100644 --- a/homeassistant/components/deconz/.translations/es-419.json +++ b/homeassistant/components/deconz/.translations/es-419.json @@ -5,7 +5,8 @@ "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en progreso.", "no_bridges": "No se descubrieron puentes deCONZ", "not_deconz_bridge": "No es un puente deCONZ", - "one_instance_only": "El componente solo admite una instancia deCONZ" + "one_instance_only": "El componente solo admite una instancia deCONZ", + "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, "error": { "no_key": "No se pudo obtener una clave de API" @@ -16,7 +17,8 @@ "allow_clip_sensor": "Permitir la importaci\u00f3n de sensores virtuales", "allow_deconz_groups": "Permitir la importaci\u00f3n de grupos deCONZ" }, - "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?" + "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?", + "title": "deCONZ Zigbee gateway a trav\u00e9s del complemento Hass.io" }, "init": { "data": { @@ -38,5 +40,35 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Ambos botones", + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "close": "Cerrar", + "left": "Izquierda", + "open": "Abrir", + "right": "Derecha", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "remote_button_rotated": "Bot\u00f3n girado \"{subtype}\"", + "remote_gyro_activated": "Dispositivo agitado" + } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Permitir sensores deCONZ CLIP", + "allow_deconz_groups": "Permitir grupos de luz deCONZ" + }, + "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/es-419.json b/homeassistant/components/heos/.translations/es-419.json index 66c02884a7e257..4d442a4543b50f 100644 --- a/homeassistant/components/heos/.translations/es-419.json +++ b/homeassistant/components/heos/.translations/es-419.json @@ -3,6 +3,11 @@ "abort": { "already_setup": "Solo puede configurar una sola conexi\u00f3n Heos, ya que ser\u00e1 compatible con todos los dispositivos de la red." }, + "step": { + "user": { + "title": "Con\u00e9ctate a Heos" + } + }, "title": "Heos" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/es-419.json b/homeassistant/components/homekit_controller/.translations/es-419.json index 9ddf336c0605c6..67a65f752b48f5 100644 --- a/homeassistant/components/homekit_controller/.translations/es-419.json +++ b/homeassistant/components/homekit_controller/.translations/es-419.json @@ -4,6 +4,7 @@ "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicie el accesorio y vuelva a intentarlo." }, + "flow_title": "Accesorio HomeKit: {name}", "step": { "pair": { "data": { diff --git a/homeassistant/components/iaqualink/.translations/es-419.json b/homeassistant/components/iaqualink/.translations/es-419.json new file mode 100644 index 00000000000000..170c2851d0801b --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/es-419.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario / direcci\u00f3n de correo electr\u00f3nico" + }, + "description": "Por favor, Ingrese el nombre de usuario y la contrase\u00f1a para su cuenta de iAqualink." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/es-419.json b/homeassistant/components/life360/.translations/es-419.json index 3f9bfab3304728..512d0285ac5e32 100644 --- a/homeassistant/components/life360/.translations/es-419.json +++ b/homeassistant/components/life360/.translations/es-419.json @@ -1,5 +1,23 @@ { "config": { + "abort": { + "user_already_configured": "La cuenta ya ha sido configurada" + }, + "error": { + "invalid_credentials": "Credenciales no v\u00e1lidas", + "invalid_username": "Nombre de usuario inv\u00e1lido", + "unexpected": "Error inesperado al comunicarse con el servidor Life360", + "user_already_configured": "La cuenta ya ha sido configurada" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Informaci\u00f3n de la cuenta Life360" + } + }, "title": "Life360" } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/es-419.json b/homeassistant/components/light/.translations/es-419.json new file mode 100644 index 00000000000000..b63f0d4445232d --- /dev/null +++ b/homeassistant/components/light/.translations/es-419.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} est\u00e1 apagada", + "is_on": "{entity_name} est\u00e1 encendida" + }, + "trigger_type": { + "turned_off": "{entity_name} desactivada", + "turned_on": "{entity_name} activada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/es-419.json b/homeassistant/components/linky/.translations/es-419.json new file mode 100644 index 00000000000000..130a856826e9d7 --- /dev/null +++ b/homeassistant/components/linky/.translations/es-419.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "La cuenta ya ha sido configurada" + }, + "error": { + "access": "No se pudo acceder a Enedis.fr, compruebe su conexi\u00f3n a Internet.", + "enedis": "Enedis.fr respondi\u00f3 con un error: vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11 p.m. y las 2 a.m.)", + "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11 p.m. y las 2 a.m.)", + "username_exists": "La cuenta ya ha sido configurada", + "wrong_login": "Error de inicio de sesi\u00f3n: por favor revise su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + }, + "description": "Ingrese sus credenciales", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/en.json b/homeassistant/components/logi_circle/.translations/en.json index 3604eb66ae20df..bf3c059f81ab9b 100644 --- a/homeassistant/components/logi_circle/.translations/en.json +++ b/homeassistant/components/logi_circle/.translations/en.json @@ -4,7 +4,7 @@ "already_setup": "You can only configure a single Logi Circle account.", "external_error": "Exception occurred from another flow.", "external_setup": "Logi Circle successfully configured from another flow.", - "no_flows": "You need to configure Logi Circle before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/logi_circle/)." + "no_flows": "You need to configure Logi Circle before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { "default": "Successfully authenticated with Logi Circle." diff --git a/homeassistant/components/nest/.translations/en.json b/homeassistant/components/nest/.translations/en.json index b68c7784d052ff..cf448bb35e7273 100644 --- a/homeassistant/components/nest/.translations/en.json +++ b/homeassistant/components/nest/.translations/en.json @@ -4,7 +4,7 @@ "already_setup": "You can only configure a single Nest account.", "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize url.", - "no_flows": "You need to configure Nest before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/nest/)." + "no_flows": "You need to configure Nest before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Internal error validating code", diff --git a/homeassistant/components/nest/.translations/es-419.json b/homeassistant/components/nest/.translations/es-419.json index 78239148a4ea5e..60a3eb65ca97e3 100644 --- a/homeassistant/components/nest/.translations/es-419.json +++ b/homeassistant/components/nest/.translations/es-419.json @@ -6,7 +6,8 @@ "no_flows": "Debe configurar Nest antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/nest/)." }, "error": { - "invalid_code": "Codigo invalido" + "invalid_code": "Codigo invalido", + "unknown": "Error desconocido al validar el c\u00f3digo" }, "step": { "init": { diff --git a/homeassistant/components/notion/.translations/es-419.json b/homeassistant/components/notion/.translations/es-419.json index ad2f19b0668ce1..1f4968f24e131f 100644 --- a/homeassistant/components/notion/.translations/es-419.json +++ b/homeassistant/components/notion/.translations/es-419.json @@ -1,6 +1,7 @@ { "config": { "error": { + "identifier_exists": "Nombre de usuario ya registrado", "invalid_credentials": "Nombre de usuario o contrase\u00f1a inv\u00e1lidos", "no_devices": "No se han encontrado dispositivos en la cuenta." }, diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index 670bc23ca1fd3b..1da4b4b4b49364 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -5,6 +5,7 @@ "already_configured": "Denne Plex-server er allerede konfigureret", "already_in_progress": "Plex konfigureres", "invalid_import": "Importeret konfiguration er ugyldig", + "token_request_timeout": "Timeout ved hentning af token", "unknown": "Mislykkedes af ukendt \u00e5rsag" }, "error": { diff --git a/homeassistant/components/plex/.translations/es-419.json b/homeassistant/components/plex/.translations/es-419.json new file mode 100644 index 00000000000000..2fc98a70ead8b7 --- /dev/null +++ b/homeassistant/components/plex/.translations/es-419.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "all_configured": "Todos los servidores vinculados ya fueron configurados", + "already_configured": "Este servidor Plex ya est\u00e1 configurado", + "already_in_progress": "Plex se est\u00e1 configurando", + "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "token_request_timeout": "Se agot\u00f3 el tiempo de espera para obtener el token", + "unknown": "Fall\u00f3 por razones desconocidas" + }, + "error": { + "faulty_credentials": "Autorizaci\u00f3n fallida", + "no_servers": "No hay servidores vinculados a la cuenta", + "no_token": "Proporcione un token o seleccione la configuraci\u00f3n manual", + "not_found": "Servidor Plex no encontrado" + }, + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Puerto", + "ssl": "Usar SSL", + "token": "Token (si es necesario)", + "verify_ssl": "Verificar el certificado SSL" + }, + "title": "Servidor Plex" + }, + "select_server": { + "data": { + "server": "Servidor" + }, + "description": "M\u00faltiples servidores disponibles, seleccione uno:", + "title": "Seleccionar servidor Plex" + }, + "user": { + "data": { + "manual_setup": "Configuraci\u00f3n manual", + "token": "Token Plex" + }, + "title": "Conectar servidor Plex" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostrar todos los controles" + }, + "description": "Opciones para reproductores multimedia Plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index f7a6bfd9c7f1b8..a0a9d087d1e58b 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -5,6 +5,7 @@ "already_configured": "Denne Plex-serveren er allerede konfigurert", "already_in_progress": "Plex blir konfigurert", "invalid_import": "Den importerte konfigurasjonen er ugyldig", + "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Mislyktes av ukjent \u00e5rsak" }, "error": { @@ -36,7 +37,7 @@ "manual_setup": "Manuelt oppsett", "token": "Plex token" }, - "description": "Angi et Plex-token for automatisk oppsett eller Konfigurer en servern manuelt.", + "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv eller manuelt konfigurere en server.", "title": "Koble til Plex-server" } }, diff --git a/homeassistant/components/point/.translations/en.json b/homeassistant/components/point/.translations/en.json index 25f0545c340942..705ac59b98d010 100644 --- a/homeassistant/components/point/.translations/en.json +++ b/homeassistant/components/point/.translations/en.json @@ -5,7 +5,7 @@ "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize url.", "external_setup": "Point successfully configured from another flow.", - "no_flows": "You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/point/)." + "no_flows": "You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/point/)." }, "create_entry": { "default": "Successfully authenticated with Minut for your Point device(s)" diff --git a/homeassistant/components/ps4/.translations/en.json b/homeassistant/components/ps4/.translations/en.json index 3a7223ade29d99..756eb65d4f7c58 100644 --- a/homeassistant/components/ps4/.translations/en.json +++ b/homeassistant/components/ps4/.translations/en.json @@ -4,8 +4,8 @@ "credential_error": "Error fetching credentials.", "devices_configured": "All devices found are already configured.", "no_devices_found": "No PlayStation 4 devices found on the network.", - "port_987_bind_error": "Could not bind to port 987. Refer to the [documentation](https://www.home-assistant.io/integrations/ps4/) for additional info.", - "port_997_bind_error": "Could not bind to port 997. Refer to the [documentation](https://www.home-assistant.io/integrations/ps4/) for additional info." + "port_987_bind_error": "Could not bind to port 987. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", + "port_997_bind_error": "Could not bind to port 997. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info." }, "error": { "credential_timeout": "Credential service timed out. Press submit to restart.", @@ -25,7 +25,7 @@ "name": "Name", "region": "Region" }, - "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed. Refer to the [documentation](https://www.home-assistant.io/integrations/ps4/) for additional info.", + "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/soma/.translations/da.json b/homeassistant/components/soma/.translations/da.json index 460f01e301feba..a82da0ce24db9f 100644 --- a/homeassistant/components/soma/.translations/da.json +++ b/homeassistant/components/soma/.translations/da.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_setup": "Du kan kun konfigurere en Soma-konto.", + "authorize_url_timeout": "Timeout ved generering af autoriseret url.", "missing_configuration": "Soma-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen." }, "create_entry": { diff --git a/homeassistant/components/somfy/.translations/es-419.json b/homeassistant/components/somfy/.translations/es-419.json new file mode 100644 index 00000000000000..ff0383c7f015cf --- /dev/null +++ b/homeassistant/components/somfy/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Somfy" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/es-419.json b/homeassistant/components/switch/.translations/es-419.json new file mode 100644 index 00000000000000..f960785203606b --- /dev/null +++ b/homeassistant/components/switch/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Desactivar {entity_name}", + "turn_on": "Activar {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido", + "turn_off": "{entity_name} apagado", + "turn_on": "{entity_name} encendido" + }, + "trigger_type": { + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/es-419.json b/homeassistant/components/tellduslive/.translations/es-419.json index 503530e728a3bb..1281784dceb93a 100644 --- a/homeassistant/components/tellduslive/.translations/es-419.json +++ b/homeassistant/components/tellduslive/.translations/es-419.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_setup": "TelldusLive ya est\u00e1 configurado", + "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", "unknown": "Se produjo un error desconocido" }, "error": { @@ -17,6 +18,7 @@ "host": "Host" } } - } + }, + "title": "Telldus Live" } } \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/en.json b/homeassistant/components/toon/.translations/en.json index dde5165c5c17f7..cea3146a3a557f 100644 --- a/homeassistant/components/toon/.translations/en.json +++ b/homeassistant/components/toon/.translations/en.json @@ -4,7 +4,7 @@ "client_id": "The client ID from the configuration is invalid.", "client_secret": "The client secret from the configuration is invalid.", "no_agreements": "This account has no Toon displays.", - "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/toon/).", + "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "Unexpected error occured, while authenticating." }, "error": { diff --git a/homeassistant/components/toon/.translations/es-419.json b/homeassistant/components/toon/.translations/es-419.json index a0ce81495a8bbb..598bc77aee9bdb 100644 --- a/homeassistant/components/toon/.translations/es-419.json +++ b/homeassistant/components/toon/.translations/es-419.json @@ -1,17 +1,27 @@ { "config": { "abort": { + "no_agreements": "Esta cuenta no tiene pantallas Toon.", "unknown_auth_fail": "Ocurri\u00f3 un error inesperado, mientras se autenticaba." }, "error": { - "credentials": "Las credenciales proporcionadas no son v\u00e1lidas." + "credentials": "Las credenciales proporcionadas no son v\u00e1lidas.", + "display_exists": "La pantalla seleccionada ya est\u00e1 configurada." }, "step": { "authenticate": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" - } + }, + "title": "Vincula tu cuenta de Toon" + }, + "display": { + "data": { + "display": "Elegir pantalla" + }, + "description": "Seleccione la pantalla Toon para conectarse.", + "title": "Seleccionar pantalla" } }, "title": "Toon" diff --git a/homeassistant/components/traccar/.translations/es-419.json b/homeassistant/components/traccar/.translations/es-419.json new file mode 100644 index 00000000000000..bfe62cc4e78466 --- /dev/null +++ b/homeassistant/components/traccar/.translations/es-419.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes de Traccar.", + "one_instance_allowed": "Solo una instancia es necesaria." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/es-419.json b/homeassistant/components/twentemilieu/.translations/es-419.json new file mode 100644 index 00000000000000..02ac8ecf27a2e9 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/es-419.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/es-419.json b/homeassistant/components/velbus/.translations/es-419.json new file mode 100644 index 00000000000000..1e1e8897c30155 --- /dev/null +++ b/homeassistant/components/velbus/.translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Este puerto ya est\u00e1 configurado" + }, + "error": { + "connection_failed": "La conexi\u00f3n velbus fall\u00f3", + "port_exists": "Este puerto ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "name": "El nombre de esta conexi\u00f3n velbus", + "port": "Cadena de conexi\u00f3n" + }, + "title": "Definir el tipo de conexi\u00f3n velbus" + } + }, + "title": "Interfaz Velbus" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/es-419.json b/homeassistant/components/vesync/.translations/es-419.json new file mode 100644 index 00000000000000..58c62fb64b60a3 --- /dev/null +++ b/homeassistant/components/vesync/.translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Solo se permite una instancia de Vesync" + }, + "error": { + "invalid_login": "Nombre de usuario o contrase\u00f1a inv\u00e1lidos" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Ingrese nombre de usuario y contrase\u00f1a" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/es-419.json b/homeassistant/components/withings/.translations/es-419.json new file mode 100644 index 00000000000000..485150d29288c1 --- /dev/null +++ b/homeassistant/components/withings/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "no_flows": "Debe configurar Withings antes de poder autenticarse con \u00e9l. Por favor lea la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado correctamente con Withings para el perfil seleccionado." + }, + "step": { + "user": { + "data": { + "profile": "Perfil" + }, + "title": "Perfil del usuario." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/es-419.json b/homeassistant/components/zha/.translations/es-419.json index 0047c762a9de0d..edf38b4fd3bdf7 100644 --- a/homeassistant/components/zha/.translations/es-419.json +++ b/homeassistant/components/zha/.translations/es-419.json @@ -16,5 +16,23 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "left": "Izquierda", + "open": "Abrir", + "right": "Derecha", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "device_dropped": "Dispositivo ca\u00eddo", + "device_flipped": "Dispositivo volteado \"{subtype}\"", + "device_knocked": "Dispositivo golpeado \"{subtype}\"", + "device_rotated": "Dispositivo girado \"{subtype}\"", + "device_shaken": "Dispositivo agitado", + "device_slid": "Dispositivo deslizado \"{subtype}\"", + "device_tilted": "Dispositivo inclinado" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index cd618072592422..2f6f42311c3490 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -16,5 +16,10 @@ } }, "title": "Zigbee Home Automation" + }, + "device_automation": { + "action_type": { + "warn": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435" + } } } \ No newline at end of file From 3e9974324480fe113003b0c80a5be267d017691c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 06:14:35 +0200 Subject: [PATCH 268/296] Add device trigger support to sensor entities (#27133) * Add device trigger support to sensor entities * Fix typing * Fix tests, add test helper for comparing lists --- .../components/automation/numeric_state.py | 6 +- .../binary_sensor/device_trigger.py | 6 +- .../device_automation/toggle_entity.py | 4 +- .../components/sensor/device_trigger.py | 145 +++++++ homeassistant/components/sensor/strings.json | 26 ++ tests/common.py | 83 ++++ .../components/deconz/test_device_trigger.py | 11 +- .../components/sensor/test_device_trigger.py | 368 ++++++++++++++++++ tests/conftest.py | 7 +- .../custom_components/test/sensor.py | 44 +++ 10 files changed, 689 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/sensor/device_trigger.py create mode 100644 homeassistant/components/sensor/strings.json create mode 100644 tests/components/sensor/test_device_trigger.py create mode 100644 tests/testing_config/custom_components/test/sensor.py diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 9dd4657291dc21..8d88fe9cae6823 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -40,7 +40,9 @@ _LOGGER = logging.getLogger(__name__) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="numeric_state" +): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) @@ -84,7 +86,7 @@ def call_action(): action( { "trigger": { - "platform": "numeric_state", + "platform": platform_type, "entity_id": entity, "below": below, "above": above, diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index c4d2efcb63b2bb..89fd9add69a940 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -195,8 +195,8 @@ async def async_attach_trigger(hass, config, action, automation_info): state_automation.CONF_FROM: from_state, state_automation.CONF_TO: to_state, } - if "for" in config: - state_config["for"] = config["for"] + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] return await state_automation.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" @@ -215,7 +215,7 @@ async def async_get_triggers(hass, device_id): ] for entry in entries: - device_class = None + device_class = DEVICE_CLASS_NONE state = hass.states.get(entry.entity_id) if state: device_class = state.attributes.get(ATTR_DEVICE_CLASS) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 7c68be83ba30ec..c9588c1efa7385 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -155,8 +155,8 @@ async def async_attach_trigger( state.CONF_FROM: from_state, state.CONF_TO: to_state, } - if "for" in config: - state_config["for"] = config["for"] + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] return await state.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py new file mode 100644 index 00000000000000..1074236eedf843 --- /dev/null +++ b/homeassistant/components/sensor/device_trigger.py @@ -0,0 +1,145 @@ +"""Provides device triggers for sensors.""" +import voluptuous as vol + +import homeassistant.components.automation.numeric_state as numeric_state_automation +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_ABOVE, + CONF_BELOW, + CONF_ENTITY_ID, + CONF_FOR, + CONF_TYPE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, +) +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import config_validation as cv + +from . import DOMAIN + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_BATTERY_LEVEL = "battery_level" +CONF_HUMIDITY = "humidity" +CONF_ILLUMINANCE = "illuminance" +CONF_POWER = "power" +CONF_PRESSURE = "pressure" +CONF_SIGNAL_STRENGTH = "signal_strength" +CONF_TEMPERATURE = "temperature" +CONF_TIMESTAMP = "timestamp" +CONF_VALUE = "value" + +ENTITY_TRIGGERS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}], + DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], + DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWER}], + DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_PRESSURE}], + DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}], + DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}], + DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_TIMESTAMP}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}], +} + + +TRIGGER_SCHEMA = vol.All( + TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In( + [ + CONF_BATTERY_LEVEL, + CONF_HUMIDITY, + CONF_ILLUMINANCE, + CONF_POWER, + CONF_PRESSURE, + CONF_SIGNAL_STRENGTH, + CONF_TEMPERATURE, + CONF_TIMESTAMP, + CONF_VALUE, + ] + ), + vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_FOR): vol.Any( + vol.All(cv.time_period, cv.positive_timedelta), + cv.template, + cv.template_complex, + ), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + numeric_state_config = { + numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE), + numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW), + numeric_state_automation.CONF_FOR: config.get(CONF_FOR), + } + if CONF_FOR in config: + numeric_state_config[CONF_FOR] = config[CONF_FOR] + + return await numeric_state_automation.async_attach_trigger( + hass, numeric_state_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + triggers = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + if state: + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + templates = ENTITY_TRIGGERS.get( + device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] + ) + + triggers.extend( + ( + { + **automation, + "platform": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for automation in templates + ) + ) + + return triggers + + +async def async_get_trigger_capabilities(hass, trigger): + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json new file mode 100644 index 00000000000000..7df239facde721 --- /dev/null +++ b/homeassistant/components/sensor/strings.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} battery level", + "is_humidity": "{entity_name} humidity", + "is_illuminance": "{entity_name} illuminance", + "is_power": "{entity_name} power", + "is_pressure": "{entity_name} pressure", + "is_signal_strength": "{entity_name} signal strength", + "is_temperature": "{entity_name} temperature", + "is_timestamp": "{entity_name} timestamp", + "is_value": "{entity_name} value" + }, + "trigger_type": { + "battery_level": "{entity_name} battery level", + "humidity": "{entity_name} humidity", + "illuminance": "{entity_name} illuminance", + "power": "{entity_name} power", + "pressure": "{entity_name} pressure", + "signal_strength": "{entity_name} signal strength", + "temperature": "{entity_name} temperature", + "timestamp": "{entity_name} timestamp", + "value": "{entity_name} value" + } + } +} diff --git a/tests/common.py b/tests/common.py index bd611e04c37a6e..0684e6daafcbef 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,5 +1,6 @@ """Test the helper method for writing tests.""" import asyncio +import collections import functools as ft import json import logging @@ -1050,3 +1051,85 @@ def mock_signal_handler(*args): hass.helpers.dispatcher.async_dispatcher_connect(signal, mock_signal_handler) return calls + + +class hashdict(dict): + """ + hashable dict implementation, suitable for use as a key into other dicts. + + >>> h1 = hashdict({"apples": 1, "bananas":2}) + >>> h2 = hashdict({"bananas": 3, "mangoes": 5}) + >>> h1+h2 + hashdict(apples=1, bananas=3, mangoes=5) + >>> d1 = {} + >>> d1[h1] = "salad" + >>> d1[h1] + 'salad' + >>> d1[h2] + Traceback (most recent call last): + ... + KeyError: hashdict(bananas=3, mangoes=5) + + based on answers from + http://stackoverflow.com/questions/1151658/python-hashable-dicts + + """ + + def __key(self): # noqa: D105 no docstring + return tuple(sorted(self.items())) + + def __repr__(self): # noqa: D105 no docstring + return ", ".join("{0}={1}".format(str(i[0]), repr(i[1])) for i in self.__key()) + + def __hash__(self): # noqa: D105 no docstring + return hash(self.__key()) + + def __setitem__(self, key, value): # noqa: D105 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def __delitem__(self, key): # noqa: D105 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def clear(self): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def pop(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def popitem(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def setdefault(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def update(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + # update is not ok because it mutates the object + # __add__ is ok because it creates a new object + # while the new object is under construction, it's ok to mutate it + def __add__(self, right): # noqa: D105 no docstring + result = hashdict(self) + dict.update(result, right) + return result + + +def assert_lists_same(a, b): + """Compare two lists, ignoring order.""" + assert collections.Counter([hashdict(i) for i in a]) == collections.Counter( + [hashdict(i) for i in b] + ) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 4677ea8d5a7c77..91714e647bd9d3 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -3,7 +3,7 @@ from homeassistant.components.deconz import device_trigger -from tests.common import async_get_device_automations +from tests.common import assert_lists_same, async_get_device_automations from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration @@ -83,6 +83,13 @@ async def test_get_triggers(hass): "type": device_trigger.CONF_LONG_RELEASE, "subtype": device_trigger.CONF_TURN_OFF, }, + { + "device_id": device_id, + "domain": "sensor", + "entity_id": "sensor.tradfri_on_off_switch_battery_level", + "platform": "device", + "type": "battery_level", + }, ] - assert triggers == expected_triggers + assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py new file mode 100644 index 00000000000000..1dc41f5bffa247 --- /dev/null +++ b/tests/components/sensor/test_device_trigger.py @@ -0,0 +1,368 @@ +"""The test for sensor device automation.""" +from datetime import timedelta +import pytest + +from homeassistant.components.sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS +from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, + async_get_device_automation_capabilities, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": trigger["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for trigger in ENTITY_TRIGGERS[device_class] + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_get_trigger_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a binary_sensor trigger.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + +async def test_if_fires_not_on_above_below(hass, calls, caplog): + """Test for value triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + }, + "action": {"service": "test.automation"}, + } + ] + }, + ) + assert "must contain at least one of below, above" in caplog.text + + +async def test_if_fires_on_state_above(hass, calls): + """Test for value triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + "above": 10, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "bat_low device - {} - 9 - 11 - None".format( + sensor1.entity_id + ) + + +async def test_if_fires_on_state_below(hass, calls): + """Test for value triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + "below": 10, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "bat_low device - {} - 11 - 9 - None".format( + sensor1.entity_id + ) + + +async def test_if_fires_on_state_between(hass, calls): + """Test for value triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + "above": 10, + "below": 20, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "bat_low device - {} - 9 - 11 - None".format( + sensor1.entity_id + ) + + hass.states.async_set(sensor1.entity_id, 21) + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.states.async_set(sensor1.entity_id, 19) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "bat_low device - {} - 21 - 19 - None".format( + sensor1.entity_id + ) + + +async def test_if_fires_on_state_change_with_for(hass, calls): + """Test for triggers firing with delay.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + "above": 10, + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + await hass.async_block_till_done() + assert calls[0].data[ + "some" + ] == "turn_off device - {} - unknown - 11 - 0:00:05".format(sensor1.entity_id) diff --git a/tests/conftest.py b/tests/conftest.py index 36c0f52f41a6b8..5e1bbc76fb5641 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,8 @@ from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.providers import legacy_api_password, homeassistant -from tests.common import ( +pytest.register_assert_rewrite("tests.common") +from tests.common import ( # noqa: E402 module level import not at top of file async_test_home_assistant, INSTANCES, mock_coro, @@ -21,7 +22,9 @@ MockUser, CLIENT_ID, ) -from tests.test_util.aiohttp import mock_aiohttp_client +from tests.test_util.aiohttp import ( + mock_aiohttp_client, +) # noqa: E402 module level import not at top of file if os.environ.get("UVLOOP") == "1": import uvloop diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py new file mode 100644 index 00000000000000..c25be28fdb03ee --- /dev/null +++ b/tests/testing_config/custom_components/test/sensor.py @@ -0,0 +1,44 @@ +""" +Provide a mock sensor platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.sensor import DEVICE_CLASSES +from tests.common import MockEntity + + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + device_class: MockSensor( + name=f"{device_class} sensor", + unique_id=f"unique_{device_class}", + device_class=device_class, + ) + for device_class in DEVICE_CLASSES + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockSensor(MockEntity): + """Mock Sensor class.""" + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._handle("device_class") From f184bf4d8506c47db0860c2497900c5c44c17233 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Oct 2019 04:02:38 -0700 Subject: [PATCH 269/296] Add Google Report State (#27112) * Add Google Report State * UPDATE codeowners" * Add config option for dev mode * update library * lint * Bug fixes --- CODEOWNERS | 2 + homeassistant/components/alexa/manifest.json | 6 +- homeassistant/components/cloud/__init__.py | 4 +- homeassistant/components/cloud/client.py | 16 ++- homeassistant/components/cloud/const.py | 3 + .../components/cloud/google_config.py | 113 +++++++++++++++- homeassistant/components/cloud/http_api.py | 18 +-- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/components/cloud/prefs.py | 10 ++ .../components/google_assistant/__init__.py | 7 + .../components/google_assistant/helpers.py | 89 ++++++++++++- .../components/google_assistant/http.py | 10 +- .../components/google_assistant/manifest.json | 6 +- .../google_assistant/report_state.py | 39 ++++++ .../components/google_assistant/smart_home.py | 3 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cloud/test_alexa_config.py | 8 +- tests/components/cloud/test_google_config.py | 121 ++++++++++++++++++ tests/components/cloud/test_http_api.py | 5 +- tests/components/cloud/test_init.py | 17 +++ tests/components/google_assistant/__init__.py | 8 +- .../components/google_assistant/test_init.py | 6 +- .../google_assistant/test_report_state.py | 47 +++++++ .../google_assistant/test_smart_home.py | 20 ++- 26 files changed, 510 insertions(+), 56 deletions(-) create mode 100644 homeassistant/components/google_assistant/report_state.py create mode 100644 tests/components/cloud/test_google_config.py create mode 100644 tests/components/google_assistant/test_report_state.py diff --git a/CODEOWNERS b/CODEOWNERS index 2bfebf145dfada..4e7b0a0cd2ab83 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -16,6 +16,7 @@ homeassistant/scripts/check_config.py @kellerza homeassistant/components/adguard/* @frenck homeassistant/components/airvisual/* @bachya homeassistant/components/alarm_control_panel/* @colinodell +homeassistant/components/alexa/* @home-assistant/cloud homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/ambiclimate/* @danielhiversen @@ -106,6 +107,7 @@ homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff homeassistant/components/gntp/* @robbiet480 +homeassistant/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton homeassistant/components/google_translate/* @awarecan homeassistant/components/google_travel_time/* @robbiet480 diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index c6629982d53fae..9db7e270e6137b 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -3,8 +3,6 @@ "name": "Alexa", "documentation": "https://www.home-assistant.io/integrations/alexa", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [] + "dependencies": ["http"], + "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 8b295634c99e58..71550fc37b14ba 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -34,6 +34,7 @@ CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL, CONF_USER_POOL_ID, + CONF_GOOGLE_ACTIONS_REPORT_STATE_URL, DOMAIN, MODE_DEV, MODE_PROD, @@ -96,7 +97,8 @@ vol.Optional(CONF_ACME_DIRECTORY_SERVER): vol.Url(), vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, - vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): str, + vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): vol.Url(), + vol.Optional(CONF_GOOGLE_ACTIONS_REPORT_STATE_URL): vol.Url(), } ) }, diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 07882d8dac202c..38ae09ced9399b 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -98,7 +98,7 @@ def google_config(self) -> google_config.CloudGoogleConfig: if not self._google_config: assert self.cloud is not None self._google_config = google_config.CloudGoogleConfig( - self.google_user_config, self._prefs, self.cloud + self._hass, self.google_user_config, self._prefs, self.cloud ) return self._google_config @@ -107,13 +107,17 @@ async def async_initialize(self, cloud) -> None: """Initialize the client.""" self.cloud = cloud - if not self.alexa_config.should_report_state or not self.cloud.is_logged_in: + if not self.cloud.is_logged_in: return - try: - await self.alexa_config.async_enable_proactive_mode() - except alexa_errors.NoTokenAvailable: - pass + if self.alexa_config.should_report_state: + try: + await self.alexa_config.async_enable_proactive_mode() + except alexa_errors.NoTokenAvailable: + pass + + if self.google_config.should_report_state: + self.google_config.async_enable_report_state() async def cleanups(self) -> None: """Cleanup some stuff after logout.""" diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index df1b8ef165d773..e28d75f017de74 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -9,6 +9,7 @@ PREF_CLOUDHOOKS = "cloudhooks" PREF_CLOUD_USER = "cloud_user" PREF_GOOGLE_ENTITY_CONFIGS = "google_entity_configs" +PREF_GOOGLE_REPORT_STATE = "google_report_state" PREF_ALEXA_ENTITY_CONFIGS = "alexa_entity_configs" PREF_ALEXA_REPORT_STATE = "alexa_report_state" PREF_OVERRIDE_NAME = "override_name" @@ -18,6 +19,7 @@ DEFAULT_SHOULD_EXPOSE = True DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False +DEFAULT_GOOGLE_REPORT_STATE = False CONF_ALEXA = "alexa" CONF_ALIASES = "aliases" @@ -33,6 +35,7 @@ CONF_REMOTE_API_URL = "remote_api_url" CONF_ACME_DIRECTORY_SERVER = "acme_directory_server" CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url" +CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url" MODE_DEV = "development" MODE_PROD = "production" diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 8986f8f399547d..38e4aec56e07ca 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -1,6 +1,13 @@ """Google config for Cloud.""" +import asyncio +import logging + +import async_timeout +from hass_nabucasa.google_report_state import ErrorResponse + from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.components.google_assistant.helpers import AbstractConfig +from homeassistant.helpers import entity_registry from .const import ( PREF_SHOULD_EXPOSE, @@ -10,15 +17,31 @@ DEFAULT_DISABLE_2FA, ) +_LOGGER = logging.getLogger(__name__) + class CloudGoogleConfig(AbstractConfig): """HA Cloud Configuration for Google Assistant.""" - def __init__(self, config, prefs, cloud): - """Initialize the Alexa config.""" + def __init__(self, hass, config, prefs, cloud): + """Initialize the Google config.""" + super().__init__(hass) self._config = config self._prefs = prefs self._cloud = cloud + self._cur_entity_prefs = self._prefs.google_entity_configs + self._sync_entities_lock = asyncio.Lock() + + prefs.async_listen_updates(self._async_prefs_updated) + hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + self._handle_entity_registry_updated, + ) + + @property + def enabled(self): + """Return if Google is enabled.""" + return self._prefs.google_enabled @property def agent_user_id(self): @@ -35,16 +58,25 @@ def secure_devices_pin(self): """Return entity config.""" return self._prefs.google_secure_devices_pin + @property + def should_report_state(self): + """Return if states should be proactively reported.""" + return self._prefs.google_report_state + def should_expose(self, state): - """If an entity should be exposed.""" - if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: + """If a state object should be exposed.""" + return self._should_expose_entity_id(state.entity_id) + + def _should_expose_entity_id(self, entity_id): + """If an entity ID should be exposed.""" + if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: return False if not self._config["filter"].empty_filter: - return self._config["filter"](state.entity_id) + return self._config["filter"](entity_id) entity_configs = self._prefs.google_entity_configs - entity_config = entity_configs.get(state.entity_id, {}) + entity_config = entity_configs.get(entity_id, {}) return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) def should_2fa(self, state): @@ -52,3 +84,72 @@ def should_2fa(self, state): entity_configs = self._prefs.google_entity_configs entity_config = entity_configs.get(state.entity_id, {}) return not entity_config.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) + + async def async_report_state(self, message): + """Send a state report to Google.""" + try: + await self._cloud.google_report_state.async_send_message(message) + except ErrorResponse as err: + _LOGGER.warning("Error reporting state - %s: %s", err.code, err.message) + + async def _async_request_sync_devices(self): + """Trigger a sync with Google.""" + if self._sync_entities_lock.locked(): + return 200 + + websession = self.hass.helpers.aiohttp_client.async_get_clientsession() + + async with self._sync_entities_lock: + with async_timeout.timeout(10): + await self._cloud.auth.async_check_token() + + _LOGGER.debug("Requesting sync") + + with async_timeout.timeout(30): + req = await websession.post( + self._cloud.google_actions_sync_url, + headers={"authorization": self._cloud.id_token}, + ) + _LOGGER.debug("Finished requesting syncing: %s", req.status) + return req.status + + async def async_deactivate_report_state(self): + """Turn off report state and disable further state reporting. + + Called when the user disconnects their account from Google. + """ + await self._prefs.async_update(google_report_state=False) + + async def _async_prefs_updated(self, prefs): + """Handle updated preferences.""" + if self.should_report_state != self.is_reporting_state: + if self.should_report_state: + self.async_enable_report_state() + else: + self.async_disable_report_state() + + # State reporting is reported as a property on entities. + # So when we change it, we need to sync all entities. + await self.async_sync_entities() + return + + # If entity prefs are the same or we have filter in config.yaml, + # don't sync. + if ( + self._cur_entity_prefs is prefs.google_entity_configs + or not self._config["filter"].empty_filter + ): + return + + self.async_schedule_google_sync() + + async def _handle_entity_registry_updated(self, event): + """Handle when entity registry updated.""" + if not self.enabled or not self._cloud.is_logged_in: + return + + entity_id = event.data["entity_id"] + + # Schedule a sync if a change was made to an entity that Google knows about + if self._should_expose_entity_id(entity_id): + await self.async_sync_entities() diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index fce530ddce5dc7..f243eab8fd0df4 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -7,6 +7,7 @@ import aiohttp import async_timeout import voluptuous as vol +from hass_nabucasa import Cloud from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView @@ -28,6 +29,7 @@ InvalidTrustedNetworks, InvalidTrustedProxies, PREF_ALEXA_REPORT_STATE, + PREF_GOOGLE_REPORT_STATE, RequireRelink, ) @@ -171,18 +173,9 @@ class GoogleActionsSyncView(HomeAssistantView): async def post(self, request): """Trigger a Google Actions sync.""" hass = request.app["hass"] - cloud = hass.data[DOMAIN] - websession = hass.helpers.aiohttp_client.async_get_clientsession() - - with async_timeout.timeout(REQUEST_TIMEOUT): - await hass.async_add_job(cloud.auth.check_token) - - with async_timeout.timeout(REQUEST_TIMEOUT): - req = await websession.post( - cloud.google_actions_sync_url, headers={"authorization": cloud.id_token} - ) - - return self.json({}, status_code=req.status) + cloud: Cloud = hass.data[DOMAIN] + status = await cloud.client.google_config.async_sync_entities() + return self.json({}, status_code=status) class CloudLoginView(HomeAssistantView): @@ -366,6 +359,7 @@ async def websocket_subscription(hass, connection, msg): vol.Optional(PREF_ENABLE_GOOGLE): bool, vol.Optional(PREF_ENABLE_ALEXA): bool, vol.Optional(PREF_ALEXA_REPORT_STATE): bool, + vol.Optional(PREF_GOOGLE_REPORT_STATE): bool, vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str), } ) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index b15fa32cb1378c..c8fa6884563d41 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.17"], + "requirements": ["hass-nabucasa==0.22"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index d6e78e87e25422..a8ff775a22788a 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -20,6 +20,8 @@ PREF_ALEXA_ENTITY_CONFIGS, PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE, + PREF_GOOGLE_REPORT_STATE, + DEFAULT_GOOGLE_REPORT_STATE, InvalidTrustedNetworks, InvalidTrustedProxies, ) @@ -74,6 +76,7 @@ async def async_update( google_entity_configs=_UNDEF, alexa_entity_configs=_UNDEF, alexa_report_state=_UNDEF, + google_report_state=_UNDEF, ): """Update user preferences.""" for key, value in ( @@ -86,6 +89,7 @@ async def async_update( (PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs), (PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs), (PREF_ALEXA_REPORT_STATE, alexa_report_state), + (PREF_GOOGLE_REPORT_STATE, google_report_state), ): if value is not _UNDEF: self._prefs[key] = value @@ -164,6 +168,7 @@ def as_dict(self): PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs, PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs, PREF_ALEXA_REPORT_STATE: self.alexa_report_state, + PREF_GOOGLE_REPORT_STATE: self.google_report_state, PREF_CLOUDHOOKS: self.cloudhooks, PREF_CLOUD_USER: self.cloud_user, } @@ -196,6 +201,11 @@ def google_enabled(self): """Return if Google is enabled.""" return self._prefs[PREF_ENABLE_GOOGLE] + @property + def google_report_state(self): + """Return if Google report state is enabled.""" + return self._prefs.get(PREF_GOOGLE_REPORT_STATE, DEFAULT_GOOGLE_REPORT_STATE) + @property def google_secure_devices_pin(self): """Return if Google is allowed to unlock locks.""" diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 61e0c70b6b368e..a1252d67fff4c3 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -83,6 +83,13 @@ async def request_sync_service_handler(call: ServiceCall): try: with async_timeout.timeout(15): agent_user_id = call.data.get("agent_user_id") or call.context.user_id + + if agent_user_id is None: + _LOGGER.warning( + "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id." + ) + return + res = await websession.post( REQUEST_SYNC_BASE_URL, params={"key": api_key}, diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index daaf790a0c1521..207194d79ed0e1 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -3,7 +3,8 @@ from collections.abc import Mapping from typing import List -from homeassistant.core import Context, callback +from homeassistant.core import Context, callback, HomeAssistant, State +from homeassistant.helpers.event import async_call_later from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, @@ -22,10 +23,24 @@ ) from .error import SmartHomeError +SYNC_DELAY = 15 + class AbstractConfig: """Hold the configuration for Google Assistant.""" + _unsub_report_state = None + + def __init__(self, hass): + """Initialize abstract config.""" + self.hass = hass + self._google_sync_unsub = None + + @property + def enabled(self): + """Return if Google is enabled.""" + return False + @property def agent_user_id(self): """Return Agent User Id to use for query responses.""" @@ -41,6 +56,17 @@ def secure_devices_pin(self): """Return entity config.""" return None + @property + def is_reporting_state(self): + """Return if we're actively reporting states.""" + return self._unsub_report_state is not None + + @property + def should_report_state(self): + """Return if states should be proactively reported.""" + # pylint: disable=no-self-use + return False + def should_expose(self, state) -> bool: """Return if entity should be exposed.""" raise NotImplementedError @@ -50,11 +76,66 @@ def should_2fa(self, state): # pylint: disable=no-self-use return True + async def async_report_state(self, message): + """Send a state report to Google.""" + raise NotImplementedError + + def async_enable_report_state(self): + """Enable proactive mode.""" + # Circular dep + from .report_state import async_enable_report_state + + if self._unsub_report_state is None: + self._unsub_report_state = async_enable_report_state(self.hass, self) + + def async_disable_report_state(self): + """Disable report state.""" + if self._unsub_report_state is not None: + self._unsub_report_state() + self._unsub_report_state = None + + async def async_sync_entities(self): + """Sync all entities to Google.""" + # Remove any pending sync + if self._google_sync_unsub: + self._google_sync_unsub() + self._google_sync_unsub = None + + return await self._async_request_sync_devices() + + async def _schedule_callback(self, _now): + """Handle a scheduled sync callback.""" + self._google_sync_unsub = None + await self.async_sync_entities() + + @callback + def async_schedule_google_sync(self): + """Schedule a sync.""" + if self._google_sync_unsub: + self._google_sync_unsub() + + self._google_sync_unsub = async_call_later( + self.hass, SYNC_DELAY, self._schedule_callback + ) + + async def _async_request_sync_devices(self) -> int: + """Trigger a sync with Google. + + Return value is the HTTP status code of the sync request. + """ + raise NotImplementedError + + async def async_deactivate_report_state(self): + """Turn off report state and disable further state reporting. + + Called when the user disconnects their account from Google. + """ + class RequestData: """Hold data associated with a particular request.""" - def __init__(self, config, user_id, request_id): + def __init__(self, config: AbstractConfig, user_id: str, request_id: str): """Initialize the request data.""" self.config = config self.request_id = request_id @@ -71,7 +152,7 @@ def get_google_type(domain, device_class): class GoogleEntity: """Adaptation of Entity expressed in Google's terms.""" - def __init__(self, hass, config, state): + def __init__(self, hass: HomeAssistant, config: AbstractConfig, state: State): """Initialize a Google entity.""" self.hass = hass self.config = config @@ -139,7 +220,7 @@ async def sync_serialize(self): "name": {"name": name}, "attributes": {}, "traits": [trait.name for trait in traits], - "willReportState": False, + "willReportState": self.config.should_report_state, "type": device_type, } diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index d68650fb6384c3..aea226348b803c 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -25,10 +25,16 @@ class GoogleConfig(AbstractConfig): """Config for manual setup of Google.""" - def __init__(self, config): + def __init__(self, hass, config): """Initialize the config.""" + super().__init__(hass) self._config = config + @property + def enabled(self): + """Return if Google is enabled.""" + return True + @property def agent_user_id(self): """Return Agent User Id to use for query responses.""" @@ -77,7 +83,7 @@ def should_2fa(self, state): @callback def async_register_http(hass, cfg): """Register HTTP views for Google Assistant.""" - hass.http.register_view(GoogleAssistantView(GoogleConfig(cfg))) + hass.http.register_view(GoogleAssistantView(GoogleConfig(hass, cfg))) class GoogleAssistantView(HomeAssistantView): diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index d2e016cb5d16b7..f97977a74001bf 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -3,8 +3,6 @@ "name": "Google assistant", "documentation": "https://www.home-assistant.io/integrations/google_assistant", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [] + "dependencies": ["http"], + "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py new file mode 100644 index 00000000000000..33bb16d7830033 --- /dev/null +++ b/homeassistant/components/google_assistant/report_state.py @@ -0,0 +1,39 @@ +"""Google Report State implementation.""" +from homeassistant.core import HomeAssistant, callback +from homeassistant.const import MATCH_ALL + +from .helpers import AbstractConfig, GoogleEntity + + +@callback +def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig): + """Enable state reporting.""" + + async def async_entity_state_listener(changed_entity, old_state, new_state): + if not new_state: + return + + if not google_config.should_expose(new_state): + return + + entity = GoogleEntity(hass, google_config, new_state) + + if not entity.is_supported(): + return + + entity_data = entity.query_serialize() + + if old_state: + old_entity = GoogleEntity(hass, google_config, old_state) + + # Only report to Google if data that Google cares about has changed + if entity_data == old_entity.query_serialize(): + return + + await google_config.async_report_state( + {"devices": {"states": {changed_entity: entity_data}}} + ) + + return hass.helpers.event.async_track_state_change( + MATCH_ALL, async_entity_state_listener + ) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 6ab6d937b51e84..f9b311a3880879 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -193,11 +193,12 @@ async def handle_devices_execute(hass, data, payload): @HANDLERS.register("action.devices.DISCONNECT") -async def async_devices_disconnect(hass, data, payload): +async def async_devices_disconnect(hass, data: RequestData, payload): """Handle action.devices.DISCONNECT request. https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect """ + await data.config.async_deactivate_report_state() return None diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 29484b671ed0e1..a64e0dc38e7b75 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 -hass-nabucasa==0.17 +hass-nabucasa==0.22 home-assistant-frontend==20191002.0 importlib-metadata==0.23 jinja2>=2.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index c84b57a09f36b5..fd44b46c64bb4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -607,7 +607,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.17 +hass-nabucasa==0.22 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61d3479d8f62d3..9bc9870be10c5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.17 +hass-nabucasa==0.22 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index c8e84016a28a9f..a7c8898659a432 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -59,7 +59,7 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock): cloud_prefs, Mock( alexa_access_token_url="http://example/alexa_token", - run_executor=Mock(side_effect=mock_coro), + auth=Mock(async_check_token=Mock(side_effect=mock_coro)), websession=hass.helpers.aiohttp_client.async_get_clientsession(), ), ) @@ -160,7 +160,11 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( EVENT_ENTITY_REGISTRY_UPDATED, - {"action": "update", "entity_id": "light.kitchen"}, + { + "action": "update", + "entity_id": "light.kitchen", + "changes": ["entity_id"], + }, ) await hass.async_block_till_done() diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py new file mode 100644 index 00000000000000..43914f489d6eff --- /dev/null +++ b/tests/components/cloud/test_google_config.py @@ -0,0 +1,121 @@ +"""Test the Cloud Google Config.""" +from unittest.mock import patch, Mock + +from homeassistant.components.google_assistant import helpers as ga_helpers +from homeassistant.components.cloud import GACTIONS_SCHEMA +from homeassistant.components.cloud.google_config import CloudGoogleConfig +from homeassistant.util.dt import utcnow +from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED + +from tests.common import mock_coro, async_fire_time_changed + + +async def test_google_update_report_state(hass, cloud_prefs): + """Test Google config responds to updating preference.""" + config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None) + + with patch.object( + config, "async_sync_entities", side_effect=mock_coro + ) as mock_sync, patch( + "homeassistant.components.google_assistant.report_state.async_enable_report_state" + ) as mock_report_state: + await cloud_prefs.async_update(google_report_state=True) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 + assert len(mock_report_state.mock_calls) == 1 + + +async def test_sync_entities(aioclient_mock, hass, cloud_prefs): + """Test sync devices.""" + aioclient_mock.post("http://example.com", status=404) + config = CloudGoogleConfig( + hass, + GACTIONS_SCHEMA({}), + cloud_prefs, + Mock( + google_actions_sync_url="http://example.com", + auth=Mock(async_check_token=Mock(side_effect=mock_coro)), + ), + ) + + assert await config.async_sync_entities() == 404 + + +async def test_google_update_expose_trigger_sync(hass, cloud_prefs): + """Test Google config responds to updating exposed entities.""" + config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None) + + with patch.object( + config, "async_sync_entities", side_effect=mock_coro + ) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0): + await cloud_prefs.async_update_google_entity_config( + entity_id="light.kitchen", should_expose=True + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 + + with patch.object( + config, "async_sync_entities", side_effect=mock_coro + ) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0): + await cloud_prefs.async_update_google_entity_config( + entity_id="light.kitchen", should_expose=False + ) + await cloud_prefs.async_update_google_entity_config( + entity_id="binary_sensor.door", should_expose=True + ) + await cloud_prefs.async_update_google_entity_config( + entity_id="sensor.temp", should_expose=True + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 + + +async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): + """Test Google config responds to entity registry.""" + config = CloudGoogleConfig( + hass, GACTIONS_SCHEMA({}), cloud_prefs, hass.data["cloud"] + ) + + with patch.object( + config, "async_sync_entities", side_effect=mock_coro + ) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0): + hass.bus.async_fire( + EVENT_ENTITY_REGISTRY_UPDATED, + {"action": "create", "entity_id": "light.kitchen"}, + ) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 + + with patch.object( + config, "async_sync_entities", side_effect=mock_coro + ) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0): + hass.bus.async_fire( + EVENT_ENTITY_REGISTRY_UPDATED, + {"action": "remove", "entity_id": "light.kitchen"}, + ) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 + + with patch.object( + config, "async_sync_entities", side_effect=mock_coro + ) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0): + hass.bus.async_fire( + EVENT_ENTITY_REGISTRY_UPDATED, + { + "action": "update", + "entity_id": "light.kitchen", + "changes": ["entity_id"], + }, + ) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index d5a3395440be1a..8e03fb82b2ce62 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -33,7 +33,9 @@ @pytest.fixture() def mock_auth(): """Mock check token.""" - with patch("hass_nabucasa.auth.CognitoAuth.check_token"): + with patch( + "hass_nabucasa.auth.CognitoAuth.async_check_token", side_effect=mock_coro + ): yield @@ -357,6 +359,7 @@ async def test_websocket_status( "google_secure_devices_pin": None, "alexa_entity_configs": {}, "alexa_report_state": False, + "google_report_state": False, "remote_enabled": False, }, "alexa_entities": { diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 244c22d2486c5d..e160ea8826ab7e 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -28,6 +28,13 @@ async def test_constructor_loads_info_from_config(hass): "user_pool_id": "test-user_pool_id", "region": "test-region", "relayer": "test-relayer", + "google_actions_sync_url": "http://test-google_actions_sync_url", + "subscription_info_url": "http://test-subscription-info-url", + "cloudhook_create_url": "http://test-cloudhook_create_url", + "remote_api_url": "http://test-remote_api_url", + "alexa_access_token_url": "http://test-alexa-token-url", + "acme_directory_server": "http://test-acme-directory-server", + "google_actions_report_state_url": "http://test-google-actions-report-state-url", }, }, ) @@ -39,6 +46,16 @@ async def test_constructor_loads_info_from_config(hass): assert cl.user_pool_id == "test-user_pool_id" assert cl.region == "test-region" assert cl.relayer == "test-relayer" + assert cl.google_actions_sync_url == "http://test-google_actions_sync_url" + assert cl.subscription_info_url == "http://test-subscription-info-url" + assert cl.cloudhook_create_url == "http://test-cloudhook_create_url" + assert cl.remote_api_url == "http://test-remote_api_url" + assert cl.alexa_access_token_url == "http://test-alexa-token-url" + assert cl.acme_directory_server == "http://test-acme-directory-server" + assert ( + cl.google_actions_report_state_url + == "http://test-google-actions-report-state-url" + ) async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user): diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 12de2eaba1cdc9..8049ac4b0dbcee 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -6,9 +6,15 @@ class MockConfig(helpers.AbstractConfig): """Fake config that always exposes everything.""" def __init__( - self, *, secure_devices_pin=None, should_expose=None, entity_config=None + self, + *, + secure_devices_pin=None, + should_expose=None, + entity_config=None, + hass=None, ): """Initialize config.""" + super().__init__(hass) self._should_expose = should_expose self._secure_devices_pin = secure_devices_pin self._entity_config = entity_config or {} diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index 1c7e0201135417..9a8b9643cfe2a3 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -1,6 +1,7 @@ """The tests for google-assistant init.""" import asyncio +from homeassistant.core import Context from homeassistant.setup import async_setup_component from homeassistant.components import google_assistant as ga @@ -20,7 +21,10 @@ def test_request_sync_service(aioclient_mock, hass): assert aioclient_mock.call_count == 0 yield from hass.services.async_call( - ga.const.DOMAIN, ga.const.SERVICE_REQUEST_SYNC, blocking=True + ga.const.DOMAIN, + ga.const.SERVICE_REQUEST_SYNC, + blocking=True, + context=Context(user_id="123"), ) assert aioclient_mock.call_count == 1 diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py new file mode 100644 index 00000000000000..bd59502a3a1b58 --- /dev/null +++ b/tests/components/google_assistant/test_report_state.py @@ -0,0 +1,47 @@ +"""Test Google report state.""" +from unittest.mock import patch + +from homeassistant.components.google_assistant.report_state import ( + async_enable_report_state, +) +from . import BASIC_CONFIG + +from tests.common import mock_coro + + +async def test_report_state(hass): + """Test report state works.""" + unsub = async_enable_report_state(hass, BASIC_CONFIG) + + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report: + hass.states.async_set("light.kitchen", "on") + await hass.async_block_till_done() + + assert len(mock_report.mock_calls) == 1 + assert mock_report.mock_calls[0][1][0] == { + "devices": {"states": {"light.kitchen": {"on": True, "online": True}}} + } + + # Test that state changes that change something that Google doesn't care about + # do not trigger a state report. + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report: + hass.states.async_set( + "light.kitchen", "on", {"irrelevant": "should_be_ignored"} + ) + await hass.async_block_till_done() + + assert len(mock_report.mock_calls) == 0 + + unsub() + + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report: + hass.states.async_set("light.kitchen", "on") + await hass.async_block_till_done() + + assert len(mock_report.mock_calls) == 0 diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 6a82204a261ab9..6ecd4af446b01a 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -657,14 +657,20 @@ async def test_device_media_player(hass, device_class, google_type): async def test_query_disconnect(hass): """Test a disconnect message.""" - result = await sh.async_handle_message( - hass, - BASIC_CONFIG, - "test-agent", - {"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID}, - ) - + config = MockConfig(hass=hass) + config.async_enable_report_state() + assert config._unsub_report_state is not None + with patch.object( + config, "async_deactivate_report_state", side_effect=mock_coro + ) as mock_deactivate: + result = await sh.async_handle_message( + hass, + config, + "test-agent", + {"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID}, + ) assert result is None + assert len(mock_deactivate.mock_calls) == 1 async def test_trait_execute_adding_query_data(hass): From bd7adf9585e588e479edc88653bf0df5fcfb2d8e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 3 Oct 2019 11:15:43 +0000 Subject: [PATCH 270/296] Bump version 0.100.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d8a68a9707ad4..b3f99038b5947e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From b63b207519f88230d325fe0e2c711f00848eb13d Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 4 Oct 2019 01:10:26 +0100 Subject: [PATCH 271/296] Handle all single zone thermostats (#27168) --- homeassistant/components/evohome/climate.py | 17 ++++++++--------- .../components/evohome/water_heater.py | 4 +++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index e5c8c6af14bde1..7df2db1b17e512 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -75,21 +75,20 @@ async def async_setup_platform( loc_idx = broker.params[CONF_LOCATION_IDX] _LOGGER.debug( - "Found Location/Controller, id=%s [%s], name=%s (location_idx=%s)", - broker.tcs.systemId, + "Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)", broker.tcs.modelType, + broker.tcs.systemId, broker.tcs.location.name, loc_idx, ) - # special case of RoundThermostat (is single zone) - if broker.config["zones"][0]["modelType"] == "RoundModulation": + # special case of RoundModulation/RoundWireless (is a single zone system) + if broker.config["zones"][0]["zoneType"] == "Thermostat": zone = list(broker.tcs.zones.values())[0] _LOGGER.debug( - "Found %s, id=%s [%s], name=%s", - zone.zoneType, - zone.zoneId, + "Found the Thermostat (%s), id=%s, name=%s", zone.modelType, + zone.zoneId, zone.name, ) @@ -101,10 +100,10 @@ async def async_setup_platform( zones = [] for zone in broker.tcs.zones.values(): _LOGGER.debug( - "Found %s, id=%s [%s], name=%s", + "Found a %s (%s), id=%s, name=%s", zone.zoneType, - zone.zoneId, zone.modelType, + zone.zoneId, zone.name, ) zones.append(EvoZone(broker, zone)) diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index b65665eb2c9ad2..37bdcd82afcac7 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -34,7 +34,9 @@ async def async_setup_platform( broker = hass.data[DOMAIN]["broker"] _LOGGER.debug( - "Found %s, id: %s", broker.tcs.hotwater.zone_type, broker.tcs.hotwater.zoneId + "Found the DHW Controller (%s), id: %s", + broker.tcs.hotwater.zone_type, + broker.tcs.hotwater.zoneId, ) evo_dhw = EvoDHW(broker, broker.tcs.hotwater) From fdb603527548c5bc01513a6f6b73c480885d192d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 22:30:59 +0200 Subject: [PATCH 272/296] Only generate device trigger for sensor with unit (#27152) --- .../components/sensor/device_trigger.py | 11 ++++++++-- .../components/sensor/test_device_trigger.py | 4 +++- .../custom_components/test/sensor.py | 22 ++++++++++++++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 1074236eedf843..c8655d91c5c0af 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -5,6 +5,7 @@ from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, CONF_ABOVE, CONF_BELOW, CONF_ENTITY_ID, @@ -113,8 +114,14 @@ async def async_get_triggers(hass, device_id): for entry in entries: device_class = DEVICE_CLASS_NONE state = hass.states.get(entry.entity_id) - if state: - device_class = state.attributes.get(ATTR_DEVICE_CLASS) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + continue + + device_class = state.attributes.get(ATTR_DEVICE_CLASS) templates = ENTITY_TRIGGERS.get( device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 1dc41f5bffa247..7118c94c5c9c7d 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -2,7 +2,7 @@ from datetime import timedelta import pytest -from homeassistant.components.sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM from homeassistant.setup import async_setup_component @@ -19,6 +19,7 @@ async_get_device_automations, async_get_device_automation_capabilities, ) +from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES @pytest.fixture @@ -70,6 +71,7 @@ async def test_get_triggers(hass, device_reg, entity_reg): } for device_class in DEVICE_CLASSES for trigger in ENTITY_TRIGGERS[device_class] + if device_class != "none" ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert triggers == expected_triggers diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index c25be28fdb03ee..651ee17bd65d46 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -3,10 +3,24 @@ Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.sensor import DEVICE_CLASSES +import homeassistant.components.sensor as sensor from tests.common import MockEntity +DEVICE_CLASSES = list(sensor.DEVICE_CLASSES) +DEVICE_CLASSES.append("none") + +UNITS_OF_MEASUREMENT = { + sensor.DEVICE_CLASS_BATTERY: "%", # % of battery that is left + sensor.DEVICE_CLASS_HUMIDITY: "%", # % of humidity in the air + sensor.DEVICE_CLASS_ILLUMINANCE: "lm", # current light level (lx/lm) + sensor.DEVICE_CLASS_SIGNAL_STRENGTH: "dB", # signal strength (dB/dBm) + sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F) + sensor.DEVICE_CLASS_TIMESTAMP: "hh:mm:ss", # timestamp (ISO8601) + sensor.DEVICE_CLASS_PRESSURE: "hPa", # pressure (hPa/mbar) + sensor.DEVICE_CLASS_POWER: "kW", # power (W/kW) +} + ENTITIES = {} @@ -22,6 +36,7 @@ def init(empty=False): name=f"{device_class} sensor", unique_id=f"unique_{device_class}", device_class=device_class, + unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), ) for device_class in DEVICE_CLASSES } @@ -42,3 +57,8 @@ class MockSensor(MockEntity): def device_class(self): """Return the class of this sensor.""" return self._handle("device_class") + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of this sensor.""" + return self._handle("unit_of_measurement") From 143e42362b28680480e536a934f2678e0388e07e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 22:17:58 +0200 Subject: [PATCH 273/296] Add above and below to sensor trigger extra_fields (#27160) --- homeassistant/components/sensor/device_trigger.py | 6 +++++- tests/components/sensor/test_device_trigger.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index c8655d91c5c0af..50fb1dd5c14602 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -147,6 +147,10 @@ async def async_get_trigger_capabilities(hass, trigger): """List trigger capabilities.""" return { "extra_fields": vol.Schema( - {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + { + vol.Optional(CONF_ABOVE): vol.Coerce(float), + vol.Optional(CONF_BELOW): vol.Coerce(float), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } ) } diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 7118c94c5c9c7d..45452dc84a0dca 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -88,7 +88,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + {"name": "above", "optional": True, "type": "float"}, + {"name": "below", "optional": True, "type": "float"}, + {"name": "for", "optional": True, "type": "positive_time_period_dict"}, ] } triggers = await async_get_device_automations(hass, "trigger", device_entry.id) From 8c3f743efdd242417268cc2fb1346b6719274bb2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 4 Oct 2019 17:05:52 +0200 Subject: [PATCH 274/296] Update connect-box to fix issue with attrs (#27194) --- homeassistant/components/upc_connect/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 2cf463d1cf0b25..cd5d327f2c22ae 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "upc_connect", "name": "Upc connect", "documentation": "https://www.home-assistant.io/integrations/upc_connect", - "requirements": ["connect-box==0.2.4"], + "requirements": ["connect-box==0.2.5"], "dependencies": [], "codeowners": ["@pvizeli"] } diff --git a/requirements_all.txt b/requirements_all.txt index fd44b46c64bb4a..45296e425026e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ colorlog==4.0.2 concord232==0.15 # homeassistant.components.upc_connect -connect-box==0.2.4 +connect-box==0.2.5 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart From 756e22290d14469f00de751d552f96aed0921aae Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Oct 2019 19:17:57 +0200 Subject: [PATCH 275/296] Fix validation when automation is saved from frontend (#27195) --- homeassistant/components/automation/config.py | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 581ce6b461d844..ebbd1771e84efd 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -18,33 +18,40 @@ async def async_validate_config_item(hass, config, full_config=None): """Validate config item.""" - try: - config = PLATFORM_SCHEMA(config) + config = PLATFORM_SCHEMA(config) - triggers = [] - for trigger in config[CONF_TRIGGER]: - trigger_platform = importlib.import_module( - "..{}".format(trigger[CONF_PLATFORM]), __name__ + triggers = [] + for trigger in config[CONF_TRIGGER]: + trigger_platform = importlib.import_module( + "..{}".format(trigger[CONF_PLATFORM]), __name__ + ) + if hasattr(trigger_platform, "async_validate_trigger_config"): + trigger = await trigger_platform.async_validate_trigger_config( + hass, trigger ) - if hasattr(trigger_platform, "async_validate_trigger_config"): - trigger = await trigger_platform.async_validate_trigger_config( - hass, trigger - ) - triggers.append(trigger) - config[CONF_TRIGGER] = triggers - - if CONF_CONDITION in config: - conditions = [] - for cond in config[CONF_CONDITION]: - cond = await condition.async_validate_condition_config(hass, cond) - conditions.append(cond) - config[CONF_CONDITION] = conditions - - actions = [] - for action in config[CONF_ACTION]: - action = await script.async_validate_action_config(hass, action) - actions.append(action) - config[CONF_ACTION] = actions + triggers.append(trigger) + config[CONF_TRIGGER] = triggers + + if CONF_CONDITION in config: + conditions = [] + for cond in config[CONF_CONDITION]: + cond = await condition.async_validate_condition_config(hass, cond) + conditions.append(cond) + config[CONF_CONDITION] = conditions + + actions = [] + for action in config[CONF_ACTION]: + action = await script.async_validate_action_config(hass, action) + actions.append(action) + config[CONF_ACTION] = actions + + return config + + +async def _try_async_validate_config_item(hass, config, full_config=None): + """Validate config item.""" + try: + config = await async_validate_config_item(hass, config, full_config) except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: async_log_exception(ex, DOMAIN, full_config or config, hass) return None @@ -57,7 +64,7 @@ async def async_validate_config(hass, config): automations = [] validated_automations = await asyncio.gather( *( - async_validate_config_item(hass, p_config, config) + _try_async_validate_config_item(hass, p_config, config) for _, p_config in config_per_platform(config, DOMAIN) ) ) From 33da7d341df507d7d18655676560df81bafc16c7 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Fri, 4 Oct 2019 17:15:43 -0400 Subject: [PATCH 276/296] Fix ecobee binary sensor and sensor unique ids (#27208) * Fix sensor unique id * Fix binary sensor unique id --- homeassistant/components/ecobee/binary_sensor.py | 3 ++- homeassistant/components/ecobee/sensor.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 68d8a88df47b64..7afdbae5a28fae 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -50,7 +50,8 @@ def unique_id(self): if sensor["name"] == self.sensor_name: if "code" in sensor: return f"{sensor['code']}-{self.device_class}" - return f"{sensor['id']}-{self.device_class}" + thermostat = self.data.ecobee.get_thermostat(self.index) + return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" @property def is_on(self): diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 8cf9af0e3b445f..24ea3d281bc618 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -61,7 +61,8 @@ def unique_id(self): if sensor["name"] == self.sensor_name: if "code" in sensor: return f"{sensor['code']}-{self.device_class}" - return f"{sensor['id']}-{self.device_class}" + thermostat = self.data.ecobee.get_thermostat(self.index) + return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" @property def device_class(self): From df0a233b6452a18d438bf24101c6c9d3b22f8a9b Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 5 Oct 2019 12:44:51 -0700 Subject: [PATCH 277/296] Bump adb-shell to 0.0.4; bump androidtv to 0.0.30 (#27224) --- homeassistant/components/androidtv/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 4fd3b062a10721..e84ed35c7636f7 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.3", - "androidtv==0.0.29" + "adb-shell==0.0.4", + "androidtv==0.0.30" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index 45296e425026e7..5328ca322c7297 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.3 +adb-shell==0.0.4 # homeassistant.components.adguard adguardhome==0.2.1 @@ -200,7 +200,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.29 +androidtv==0.0.30 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bc9870be10c5c..f3cacfa88887fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -80,7 +80,7 @@ aiowwlln==2.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.29 +androidtv==0.0.30 # homeassistant.components.apns apns2==0.3.0 From 1e1f79e45b49cc3068d6cb3cfd012b67e02d1111 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 Oct 2019 13:40:29 -0700 Subject: [PATCH 278/296] Bumped version to 0.100.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b3f99038b5947e..68537aff298735 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 2ccd0039d7aca89297846c1df7b01786fb52dbf6 Mon Sep 17 00:00:00 2001 From: Pierre Sicot Date: Sat, 5 Oct 2019 22:28:19 +0200 Subject: [PATCH 279/296] Fix closed status for non horizontal awnings. (#26840) --- homeassistant/components/tahoma/cover.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index a189199bfb2b3c..7448eb27ae072f 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -137,14 +137,13 @@ def update(self): if self._closure is not None: if self.tahoma_device.type == HORIZONTAL_AWNING: self._position = self._closure - self._closed = self._position == 0 else: self._position = 100 - self._closure - self._closed = self._position == 100 if self._position <= 5: self._position = 0 if self._position >= 95: self._position = 100 + self._closed = self._position == 0 else: self._position = None if "core:OpenClosedState" in self.tahoma_device.active_states: From d39e320b9e74b494e988e46a1f62337174e32653 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Oct 2019 10:39:14 -0500 Subject: [PATCH 280/296] Fix update on cert_expiry startup (#27137) * Don't force extra update on startup * Skip on entity add instead * Conditional update based on HA state * Only force entity state update when postponed * Clean up state updating * Delay YAML import --- .../components/cert_expiry/sensor.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index b564cff7338584..a5b879e5661e85 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -15,6 +15,7 @@ CONF_PORT, EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import callback from homeassistant.helpers.entity import Entity from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT @@ -35,18 +36,26 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up certificate expiry sensor.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + + @callback + def do_import(_): + """Process YAML import after HA is fully started.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + ) ) - ) + + # Delay to avoid validation during setup in case we're checking our own cert. + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_import) async def async_setup_entry(hass, entry, async_add_entities): """Add cert-expiry entry.""" async_add_entities( [SSLCertificate(entry.title, entry.data[CONF_HOST], entry.data[CONF_PORT])], - True, + False, + # Don't update in case we're checking our own cert. ) return True @@ -84,17 +93,22 @@ def icon(self): @property def available(self): - """Icon to use in the frontend, if any.""" + """Return the availability of the sensor.""" return self._available async def async_added_to_hass(self): """Once the entity is added we should update to get the initial data loaded.""" + @callback def do_update(_): """Run the update method when the start event was fired.""" - self.update() + self.async_schedule_update_ha_state(True) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) + if self.hass.is_running: + self.async_schedule_update_ha_state(True) + else: + # Delay until HA is fully started in case we're checking our own cert. + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) def update(self): """Fetch the certificate information.""" From 8de942f00f4b11dc7a83d8add88445513789596c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Conde=20G=C3=B3mez?= Date: Sun, 6 Oct 2019 17:00:44 +0200 Subject: [PATCH 281/296] Fix onvif PTZ service freeze (#27250) --- homeassistant/components/onvif/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 0c116568780a50..29af1049faede3 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -23,7 +23,7 @@ from homeassistant.components.ffmpeg import DATA_FFMPEG, CONF_EXTRA_ARGUMENTS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.helpers.service import extract_entity_ids +from homeassistant.helpers.service import async_extract_entity_ids import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -88,7 +88,7 @@ async def async_handle_ptz(service): tilt = service.data.get(ATTR_TILT, None) zoom = service.data.get(ATTR_ZOOM, None) all_cameras = hass.data[ONVIF_DATA][ENTITIES] - entity_ids = extract_entity_ids(hass, service) + entity_ids = await async_extract_entity_ids(hass, service) target_cameras = [] if not entity_ids: target_cameras = all_cameras From c4165418149986f3316b06072a368dd34cf1c577 Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Mon, 7 Oct 2019 10:40:52 -0700 Subject: [PATCH 282/296] Fix the todoist integration (#27273) * Fixed the todoist integration. * Removing unused import * Flake8 fixes. * Added username to codeowners. * Updated global codeowners --- CODEOWNERS | 1 + homeassistant/components/todoist/calendar.py | 40 +++++++++++-------- .../components/todoist/manifest.json | 4 +- .../components/todoist/services.yaml | 25 ++++++++++++ requirements_all.txt | 2 +- 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 4e7b0a0cd2ab83..d2cda1f1d07435 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -288,6 +288,7 @@ homeassistant/components/threshold/* @fabaff homeassistant/components/tibber/* @danielhiversen homeassistant/components/tile/* @bachya homeassistant/components/time_date/* @fabaff +homeassistant/components/todoist/* @boralyl homeassistant/components/toon/* @frenck homeassistant/components/tplink/* @rytilahti homeassistant/components/traccar/* @ludeeus diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 75aec037a25ed5..1179fd9086836f 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -36,6 +36,7 @@ DESCRIPTION = "description" # Calendar Platform: Used in the '_get_date()' method DATETIME = "dateTime" +DUE = "due" # Service Call: When is this task due (in natural language)? DUE_DATE_STRING = "due_date_string" # Service Call: The language of DUE_DATE_STRING @@ -206,7 +207,7 @@ def handle_new_task(call): project_id = project_id_lookup[project_name] # Create the task - item = api.items.add(call.data[CONTENT], project_id) + item = api.items.add(call.data[CONTENT], project_id=project_id) if LABELS in call.data: task_labels = call.data[LABELS] @@ -216,11 +217,12 @@ def handle_new_task(call): if PRIORITY in call.data: item.update(priority=call.data[PRIORITY]) + _due: dict = {} if DUE_DATE_STRING in call.data: - item.update(date_string=call.data[DUE_DATE_STRING]) + _due["string"] = call.data[DUE_DATE_STRING] if DUE_DATE_LANG in call.data: - item.update(date_lang=call.data[DUE_DATE_LANG]) + _due["lang"] = call.data[DUE_DATE_LANG] if DUE_DATE in call.data: due_date = dt.parse_datetime(call.data[DUE_DATE]) @@ -231,7 +233,11 @@ def handle_new_task(call): due_date = dt.as_utc(due_date) date_format = "%Y-%m-%dT%H:%M" due_date = datetime.strftime(due_date, date_format) - item.update(due_date_utc=due_date) + _due["date"] = due_date + + if _due: + item.update(due=_due) + # Commit changes api.commit() _LOGGER.debug("Created Todoist task: %s", call.data[CONTENT]) @@ -241,6 +247,17 @@ def handle_new_task(call): ) +def _parse_due_date(data: dict) -> datetime: + """Parse the due date dict into a datetime object.""" + # Add time information to date only strings. + if len(data["date"]) == 10: + data["date"] += "T00:00:00" + # If there is no timezone provided, use UTC. + if data["timezone"] is None: + data["date"] += "Z" + return dt.parse_datetime(data["date"]) + + class TodoistProjectDevice(CalendarEventDevice): """A device for getting the next Task from a Todoist Project.""" @@ -412,16 +429,8 @@ def create_todoist_task(self, data): # complete the task. # Generally speaking, that means right now. task[START] = dt.utcnow() - if data[DUE_DATE_UTC] is not None: - due_date = data[DUE_DATE_UTC] - - # Due dates are represented in RFC3339 format, in UTC. - # Home Assistant exclusively uses UTC, so it'll - # handle the conversion. - time_format = "%a %d %b %Y %H:%M:%S %z" - # HASS' built-in parse time function doesn't like - # Todoist's time format; strptime has to be used. - task[END] = datetime.strptime(due_date, time_format) + if data[DUE] is not None: + task[END] = _parse_due_date(data[DUE]) if self._latest_due_date is not None and ( task[END] > self._latest_due_date @@ -540,9 +549,8 @@ async def async_get_events(self, hass, start_date, end_date): project_task_data = project_data[TASKS] events = [] - time_format = "%a %d %b %Y %H:%M:%S %z" for task in project_task_data: - due_date = datetime.strptime(task["due_date_utc"], time_format) + due_date = _parse_due_date(task["due"]) if start_date < due_date < end_date: event = { "uid": task["id"], diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index dbf1a941e00ba3..e7876c953cc5c9 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -3,8 +3,8 @@ "name": "Todoist", "documentation": "https://www.home-assistant.io/integrations/todoist", "requirements": [ - "todoist-python==7.0.17" + "todoist-python==8.0.0" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@boralyl"] } diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index e69de29bb2d1d6..c2d23cc4bec5a6 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -0,0 +1,25 @@ +new_task: + description: Create a new task and add it to a project. + fields: + content: + description: The name of the task. + example: Pick up the mail. + project: + description: The name of the project this task should belong to. Defaults to Inbox. + example: Errands + labels: + description: Any labels that you want to apply to this task, separated by a comma. + example: Chores,Delivieries + priority: + description: The priority of this task, from 1 (normal) to 4 (urgent). + example: 2 + due_date_string: + description: The day this task is due, in natural language. + example: Tomorrow + due_date_lang: + description: The language of due_date_string. + example: en + due_date: + description: The day this task is due, in format YYYY-MM-DD. + example: 2019-10-22 + diff --git a/requirements_all.txt b/requirements_all.txt index 5328ca322c7297..be88d6c60cb309 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1888,7 +1888,7 @@ thingspeak==0.4.1 tikteck==0.4 # homeassistant.components.todoist -todoist-python==7.0.17 +todoist-python==8.0.0 # homeassistant.components.toon toonapilib==3.2.4 From 73aa341ed880f6aeca164128a172f9a8f48e2f8b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 6 Oct 2019 23:02:58 -0500 Subject: [PATCH 283/296] Fix Plex media_player.play_media service (#27278) * First attempt to fix play_media * More changes to media playback * Use playqueues, clean up play_media * Use similar function name, add docstring --- homeassistant/components/plex/media_player.py | 139 ++++++++---------- homeassistant/components/plex/server.py | 14 ++ 2 files changed, 76 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 356c7fe5741702..a49e4c9c057ee1 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -2,10 +2,9 @@ from datetime import timedelta import json import logging +from xml.etree.ElementTree import ParseError import plexapi.exceptions -import plexapi.playlist -import plexapi.playqueue import requests.exceptions from homeassistant.components.media_player import MediaPlayerDevice @@ -16,6 +15,7 @@ SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, @@ -543,9 +543,6 @@ def make(self): @property def supported_features(self): """Flag media player features that are supported.""" - if not self._is_player_active: - return 0 - # force show all controls if self.plex_server.show_all_controls: return ( @@ -555,13 +552,11 @@ def supported_features(self): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE ) - # only show controls when we know what device is connecting - if not self._make: - return 0 # no mute support if self.make.lower() == "shield android tv": _LOGGER.debug( @@ -575,8 +570,10 @@ def supported_features(self): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF ) + # Only supports play,pause,stop (and off which really is stop) if self.make.lower().startswith("tivo"): _LOGGER.debug( @@ -585,8 +582,7 @@ def supported_features(self): self.entity_id, ) return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF - # Not all devices support playback functionality - # Playback includes volume, stop/play/pause, etc. + if self.device and "playback" in self._device_protocol_capabilities: return ( SUPPORT_PAUSE @@ -595,6 +591,7 @@ def supported_features(self): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE ) @@ -682,49 +679,74 @@ def play_media(self, media_type, media_id, **kwargs): return src = json.loads(media_id) + library = src.get("library_name") + shuffle = src.get("shuffle", 0) media = None + if media_type == "MUSIC": - media = ( - self.device.server.library.section(src["library_name"]) - .get(src["artist_name"]) - .album(src["album_name"]) - .get(src["track_name"]) - ) + media = self._get_music_media(library, src) elif media_type == "EPISODE": - media = self._get_tv_media( - src["library_name"], - src["show_name"], - src["season_number"], - src["episode_number"], - ) + media = self._get_tv_media(library, src) elif media_type == "PLAYLIST": - media = self.device.server.playlist(src["playlist_name"]) + media = self.plex_server.playlist(src["playlist_name"]) elif media_type == "VIDEO": - media = self.device.server.library.section(src["library_name"]).get( - src["video_name"] - ) + media = self.plex_server.library.section(library).get(src["video_name"]) - if ( - media - and media_type == "EPISODE" - and isinstance(media, plexapi.playlist.Playlist) - ): - # delete episode playlist after being loaded into a play queue - self._client_play_media(media=media, delete=True, shuffle=src["shuffle"]) - elif media: - self._client_play_media(media=media, shuffle=src["shuffle"]) + if media is None: + _LOGGER.error("Media could not be found: %s", media_id) + return + + playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) + try: + self.device.playMedia(playqueue) + except ParseError: + # Temporary workaround for Plexamp / plexapi issue + pass + except requests.exceptions.ConnectTimeout: + _LOGGER.error("Timed out playing on %s", self.name) + + self.update_devices() - def _get_tv_media(self, library_name, show_name, season_number, episode_number): + def _get_music_media(self, library_name, src): + """Find music media and return a Plex media object.""" + artist_name = src["artist_name"] + album_name = src.get("album_name") + track_name = src.get("track_name") + track_number = src.get("track_number") + + artist = self.plex_server.library.section(library_name).get(artist_name) + + if album_name: + album = artist.album(album_name) + + if track_name: + return album.track(track_name) + + if track_number: + for track in album.tracks(): + if int(track.index) == int(track_number): + return track + return None + + return album + + if track_name: + return artist.searchTracks(track_name, maxresults=1) + return artist + + def _get_tv_media(self, library_name, src): """Find TV media and return a Plex media object.""" + show_name = src["show_name"] + season_number = src.get("season_number") + episode_number = src.get("episode_number") target_season = None target_episode = None - show = self.device.server.library.section(library_name).get(show_name) + show = self.plex_server.library.section(library_name).get(show_name) if not season_number: - playlist_name = f"{self.entity_id} - {show_name} Episodes" - return self.device.server.createPlaylist(playlist_name, show.episodes()) + return show for season in show.seasons(): if int(season.seasonNumber) == int(season_number): @@ -741,12 +763,7 @@ def _get_tv_media(self, library_name, show_name, season_number, episode_number): ) else: if not episode_number: - playlist_name = "{} - {} Season {} Episodes".format( - self.entity_id, show_name, str(season_number) - ) - return self.device.server.createPlaylist( - playlist_name, target_season.episodes() - ) + return target_season for episode in target_season.episodes(): if int(episode.index) == int(episode_number): @@ -764,38 +781,6 @@ def _get_tv_media(self, library_name, show_name, season_number, episode_number): return target_episode - def _client_play_media(self, media, delete=False, **params): - """Instruct Plex client to play a piece of media.""" - if not (self.device and "playback" in self._device_protocol_capabilities): - _LOGGER.error("Client cannot play media: %s", self.entity_id) - return - - playqueue = plexapi.playqueue.PlayQueue.create( - self.device.server, media, **params - ) - - # Delete dynamic playlists used to build playqueue (ex. play tv season) - if delete: - media.delete() - - server_url = self.device.server.baseurl.split(":") - self.device.sendCommand( - "playback/playMedia", - **dict( - { - "machineIdentifier": self.device.server.machineIdentifier, - "address": server_url[1].strip("/"), - "port": server_url[-1], - "key": media.key, - "containerKey": "/playQueues/{}?window=100&own=1".format( - playqueue.playQueueID - ), - }, - **params, - ), - ) - self.update_devices() - @property def device_state_attributes(self): """Return the scene state attributes.""" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index d4393d38c97427..df9e9f9f6c3528 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,5 +1,6 @@ """Shared class to maintain Plex server instances.""" import plexapi.myplex +import plexapi.playqueue import plexapi.server from requests import Session @@ -109,3 +110,16 @@ def use_episode_art(self): def show_all_controls(self): """Return show_all_controls option.""" return self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] + + @property + def library(self): + """Return library attribute from server object.""" + return self._plex_server.library + + def playlist(self, title): + """Return playlist from server object.""" + return self._plex_server.playlist(title) + + def create_playqueue(self, media, **kwargs): + """Create playqueue on Plex server.""" + return plexapi.playqueue.PlayQueue.create(self._plex_server, media, **kwargs) From 463c2e8d45bf89199ea2cb64881cc38e37f14627 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Oct 2019 13:29:12 -0500 Subject: [PATCH 284/296] Remove manual config flow step (#27291) --- homeassistant/components/plex/config_flow.py | 59 +----- homeassistant/components/plex/strings.json | 18 +- tests/components/plex/test_config_flow.py | 188 ++++++------------- 3 files changed, 66 insertions(+), 199 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index dd5401950e9a3c..38727ccff067f4 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -12,14 +12,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_URL, - CONF_TOKEN, - CONF_SSL, - CONF_VERIFY_SSL, -) +from homeassistant.const import CONF_URL, CONF_TOKEN, CONF_SSL, CONF_VERIFY_SSL from homeassistant.core import callback from homeassistant.util.json import load_json @@ -30,8 +23,6 @@ CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, - DEFAULT_PORT, - DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, PLEX_CONFIG_FILE, @@ -44,8 +35,6 @@ from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer -USER_SCHEMA = vol.Schema({vol.Optional("manual_setup"): bool}) - _LOGGER = logging.getLogger(__package__) @@ -73,23 +62,17 @@ def async_get_options_flow(config_entry): def __init__(self): """Initialize the Plex flow.""" self.current_login = {} - self.discovery_info = {} self.available_servers = None self.plexauth = None self.token = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - errors = {} - if user_input is not None: - if user_input.pop("manual_setup", False): - return await self.async_step_manual_setup(user_input) + return self.async_show_form(step_id="start_website_auth") - return await self.async_step_plex_website_auth() - - return self.async_show_form( - step_id="user", data_schema=USER_SCHEMA, errors=errors - ) + async def async_step_start_website_auth(self, user_input=None): + """Show a form before starting external authentication.""" + return await self.async_step_plex_website_auth() async def async_step_server_validate(self, server_config): """Validate a provided configuration.""" @@ -120,9 +103,7 @@ async def async_step_server_validate(self, server_config): return self.async_abort(reason="unknown") if errors: - return self.async_show_form( - step_id="user", data_schema=USER_SCHEMA, errors=errors - ) + return self.async_show_form(step_id="start_website_auth", errors=errors) server_id = plex_server.machine_identifier @@ -152,30 +133,6 @@ async def async_step_server_validate(self, server_config): }, ) - async def async_step_manual_setup(self, user_input=None): - """Begin manual configuration.""" - if len(user_input) > 1: - host = user_input.pop(CONF_HOST) - port = user_input.pop(CONF_PORT) - prefix = "https" if user_input.pop(CONF_SSL) else "http" - user_input[CONF_URL] = f"{prefix}://{host}:{port}" - return await self.async_step_server_validate(user_input) - - data_schema = vol.Schema( - { - vol.Required( - CONF_HOST, default=self.discovery_info.get(CONF_HOST) - ): str, - vol.Required( - CONF_PORT, default=self.discovery_info.get(CONF_PORT, DEFAULT_PORT) - ): int, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, - vol.Optional(CONF_TOKEN, default=user_input.get(CONF_TOKEN, "")): str, - } - ) - return self.async_show_form(step_id="manual_setup", data_schema=data_schema) - async def async_step_select_server(self, user_input=None): """Use selected Plex server.""" config = dict(self.current_login) @@ -210,8 +167,6 @@ async def async_step_discovery(self, discovery_info): # Skip discovery if a config already exists or is in progress. return self.async_abort(reason="already_configured") - discovery_info[CONF_PORT] = int(discovery_info[CONF_PORT]) - self.discovery_info = discovery_info json_file = self.hass.config.path(PLEX_CONFIG_FILE) file_config = await self.hass.async_add_executor_job(load_json, json_file) @@ -227,7 +182,7 @@ async def async_step_discovery(self, discovery_info): _LOGGER.info("Imported legacy config, file can be removed: %s", json_file) return await self.async_step_server_validate(server_config) - return await self.async_step_user() + return self.async_abort(reason="discovery_no_file") async def async_step_import(self, import_config): """Import from Plex configuration.""" diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 6538d8e887e18c..aff79acc2ed4cf 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -2,16 +2,6 @@ "config": { "title": "Plex", "step": { - "manual_setup": { - "title": "Plex server", - "data": { - "host": "Host", - "port": "Port", - "ssl": "Use SSL", - "verify_ssl": "Verify SSL certificate", - "token": "Token (if required)" - } - }, "select_server": { "title": "Select Plex server", "description": "Multiple servers available, select one:", @@ -19,12 +9,9 @@ "server": "Server" } }, - "user": { + "start_website_auth": { "title": "Connect Plex server", - "description": "Continue to authorize at plex.tv or manually configure a server.", - "data": { - "manual_setup": "Manual setup" - } + "description": "Continue to authorize at plex.tv." } }, "error": { @@ -36,6 +23,7 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", + "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 753d565a82b981..e9f48f6a4f80b4 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -6,14 +6,7 @@ import requests.exceptions from homeassistant.components.plex import config_flow -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_VERIFY_SSL, - CONF_TOKEN, - CONF_URL, -) +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -48,34 +41,32 @@ def init_config_flow(hass): async def test_bad_credentials(hass): """Test when provided credentials are rejected.""" + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": True} - ) - assert result["type"] == "form" - assert result["step_id"] == "manual_setup" - - with patch( + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value="BAD TOKEN" ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: MOCK_HOST_1, - CONF_PORT: MOCK_PORT_1, - CONF_SSL: False, - CONF_VERIFY_SSL: False, - CONF_TOKEN: "BAD TOKEN", - }, + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "start_website_auth" assert result["errors"]["base"] == "faulty_credentials" @@ -123,8 +114,8 @@ async def test_discovery(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["type"] == "abort" + assert result["reason"] == "discovery_no_file" async def test_discovery_while_in_progress(hass): @@ -201,7 +192,7 @@ async def test_import_bad_hostname(hass): }, ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" assert result["errors"]["base"] == "not_found" @@ -212,26 +203,25 @@ async def test_unknown_exception(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": True} - ) - assert result["type"] == "form" - assert result["step_id"] == "manual_setup" + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.server.PlexServer", side_effect=Exception): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: MOCK_HOST_1, - CONF_PORT: MOCK_PORT_1, - CONF_SSL: True, - CONF_VERIFY_SSL: True, - CONF_TOKEN: MOCK_TOKEN, - }, - ) + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( + "plexapi.server.PlexServer", side_effect=Exception + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value="MOCK_TOKEN" + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "abort" assert result["reason"] == "unknown" @@ -245,7 +235,7 @@ async def test_no_servers_found(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mm_plex_account = MagicMock() mm_plex_account.resources = Mock(return_value=[]) @@ -256,9 +246,7 @@ async def test_no_servers_found(hass): "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -266,7 +254,7 @@ async def test_no_servers_found(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" assert result["errors"]["base"] == "no_servers" @@ -279,7 +267,7 @@ async def test_single_available_server(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() @@ -304,9 +292,7 @@ async def test_single_available_server(hass): mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -336,7 +322,7 @@ async def test_multiple_servers_with_selection(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() mm_plex_account = MagicMock() @@ -360,9 +346,7 @@ async def test_multiple_servers_with_selection(hass): mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -406,7 +390,7 @@ async def test_adding_last_unconfigured_server(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() mm_plex_account = MagicMock() @@ -430,9 +414,7 @@ async def test_adding_last_unconfigured_server(hass): mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -512,7 +494,7 @@ async def test_all_available_servers_configured(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() mm_plex_account = MagicMock() @@ -525,9 +507,7 @@ async def test_all_available_servers_configured(hass): "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -538,58 +518,6 @@ async def test_all_available_servers_configured(hass): assert result["reason"] == "all_configured" -async def test_manual_config(hass): - """Test creating via manual configuration.""" - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": True} - ) - assert result["type"] == "form" - assert result["step_id"] == "manual_setup" - - mock_connections = MockConnections(ssl=True) - - with patch("plexapi.server.PlexServer") as mock_plex_server: - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: MOCK_HOST_1, - CONF_PORT: MOCK_PORT_1, - CONF_SSL: True, - CONF_VERIFY_SSL: True, - CONF_TOKEN: MOCK_TOKEN, - }, - ) - assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name - assert ( - result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier - ) - assert ( - result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri - ) - assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN - - async def test_option_flow(hass): """Test config flow selection of one of two bridges.""" @@ -627,15 +555,13 @@ async def test_external_timed_out(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=None ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -655,14 +581,12 @@ async def test_callback_view(hass, aiohttp_client): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" client = await aiohttp_client(hass.http.app) From 1614e0d866ff7eab910e109d6d3a095eb331e050 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Oct 2019 20:08:07 -0700 Subject: [PATCH 285/296] Improve speed websocket sends messages (#27295) * Improve speed websocket sends messages * return -> continue --- homeassistant/components/websocket_api/http.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 17a6709496a4e6..08a0430ee2a976 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -61,12 +61,15 @@ async def _writer(self): message = await self._to_write.get() if message is None: break + self._logger.debug("Sending %s", message) + + if isinstance(message, str): + await self.wsock.send_str(message) + continue + try: - if isinstance(message, str): - await self.wsock.send_str(message) - else: - await self.wsock.send_json(message, dumps=JSON_DUMP) + dumped = JSON_DUMP(message) except (ValueError, TypeError) as err: self._logger.error( "Unable to serialize to JSON: %s\n%s", err, message @@ -76,6 +79,9 @@ async def _writer(self): message["id"], ERR_UNKNOWN_ERROR, "Invalid JSON in response" ) ) + continue + + await self.wsock.send_str(dumped) @callback def _send_message(self, message): From 4322310d3670854a469abc8dd5b87ca1a543541e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Oct 2019 21:28:58 -0700 Subject: [PATCH 286/296] Bumped version to 0.100.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 68537aff298735..5bd5b7ea7650be 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From c214d7a972d46cd960080cdf94e0e83d20946612 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Oct 2019 09:58:36 -0700 Subject: [PATCH 287/296] Google: Report all states on activating report state (#27312) --- .../components/google_assistant/helpers.py | 5 +++ .../google_assistant/report_state.py | 24 +++++++++++++- .../google_assistant/test_report_state.py | 31 ++++++++++++++++--- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 207194d79ed0e1..933f0c07999884 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -182,6 +182,11 @@ def traits(self): ] return self._traits + @callback + def should_expose(self): + """If entity should be exposed.""" + return self.config.should_expose(self.state) + @callback def is_supported(self) -> bool: """Return if the entity is supported by Google.""" diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 33bb16d7830033..869bc61d7a3624 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,8 +1,13 @@ """Google Report State implementation.""" from homeassistant.core import HomeAssistant, callback from homeassistant.const import MATCH_ALL +from homeassistant.helpers.event import async_call_later -from .helpers import AbstractConfig, GoogleEntity +from .helpers import AbstractConfig, GoogleEntity, async_get_entities + +# Time to wait until the homegraph updates +# https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 +INITIAL_REPORT_DELAY = 60 @callback @@ -34,6 +39,23 @@ async def async_entity_state_listener(changed_entity, old_state, new_state): {"devices": {"states": {changed_entity: entity_data}}} ) + async_call_later( + hass, INITIAL_REPORT_DELAY, _async_report_all_states(hass, google_config) + ) + return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener ) + + +async def _async_report_all_states(hass: HomeAssistant, google_config: AbstractConfig): + """Report all states.""" + entities = {} + + for entity in async_get_entities(hass, google_config): + if not entity.should_expose(): + continue + + entities[entity.entity_id] = entity.query_serialize() + + await google_config.async_report_state({"devices": {"states": entities}}) diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index bd59502a3a1b58..734d9ec7fc83af 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,17 +1,38 @@ """Test Google report state.""" from unittest.mock import patch -from homeassistant.components.google_assistant.report_state import ( - async_enable_report_state, -) +from homeassistant.components.google_assistant import report_state +from homeassistant.util.dt import utcnow + from . import BASIC_CONFIG -from tests.common import mock_coro + +from tests.common import mock_coro, async_fire_time_changed async def test_report_state(hass): """Test report state works.""" - unsub = async_enable_report_state(hass, BASIC_CONFIG) + hass.states.async_set("light.ceiling", "off") + hass.states.async_set("switch.ac", "on") + + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0): + unsub = report_state.async_enable_report_state(hass, BASIC_CONFIG) + + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + # Test that enabling report state does a report on all entities + assert len(mock_report.mock_calls) == 1 + assert mock_report.mock_calls[0][1][0] == { + "devices": { + "states": { + "light.ceiling": {"on": False, "online": True}, + "switch.ac": {"on": True, "online": True}, + } + } + } with patch.object( BASIC_CONFIG, "async_report_state", side_effect=mock_coro From 07b1976f7d40388467e2a549f043eda443734d45 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 8 Oct 2019 10:54:01 -0500 Subject: [PATCH 288/296] Fix single Plex server case (#27326) --- homeassistant/components/plex/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index df9e9f9f6c3528..6ab114307664b3 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -57,7 +57,7 @@ def _set_missing_url(): raise ServerNotSpecified(available_servers) server_choice = ( - self._server_name if self._server_name else available_servers[0] + self._server_name if self._server_name else available_servers[0][0] ) connections = account.resource(server_choice).connections local_url = [x.httpuri for x in connections if x.local] From 579c91da1b16d0b43d07062508ee2d82ab791ba2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 8 Oct 2019 18:33:14 +0200 Subject: [PATCH 289/296] Updated frontend to 20191002.1 (#27329) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 60a4f0faa9c16f..58e5558781a2ed 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191002.0" + "home-assistant-frontend==20191002.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a64e0dc38e7b75..04a68bf9633d4f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191002.0 +home-assistant-frontend==20191002.1 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index be88d6c60cb309..7aa5090688c092 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -640,7 +640,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.0 +home-assistant-frontend==20191002.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3cacfa88887fa..69c6ded580688f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -182,7 +182,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.0 +home-assistant-frontend==20191002.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 58f444c779b5e8edad4b00f12eb3efd89841695e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Oct 2019 18:57:24 +0200 Subject: [PATCH 290/296] Fix translations for binary_sensor triggers (#27330) --- .../components/binary_sensor/device_trigger.py | 16 ++++++++-------- .../components/binary_sensor/strings.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 89fd9add69a940..f138bcfd5a813d 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -81,8 +81,8 @@ CONF_NO_SOUND = "no_sound" CONF_VIBRATION = "vibration" CONF_NO_VIBRATION = "no_vibration" -CONF_OPEN = "open" -CONF_NOT_OPEN = "not_open" +CONF_OPENED = "opened" +CONF_NOT_OPENED = "not_opened" TURNED_ON = [ @@ -97,7 +97,7 @@ CONF_MOTION, CONF_MOVING, CONF_OCCUPIED, - CONF_OPEN, + CONF_OPENED, CONF_PLUGGED_IN, CONF_POWERED, CONF_PRESENT, @@ -118,7 +118,7 @@ CONF_NOT_MOIST, CONF_NOT_MOVING, CONF_NOT_OCCUPIED, - CONF_NOT_OPEN, + CONF_NOT_OPENED, CONF_NOT_PLUGGED_IN, CONF_NOT_POWERED, CONF_NOT_PRESENT, @@ -141,8 +141,8 @@ {CONF_TYPE: CONF_CONNECTED}, {CONF_TYPE: CONF_NOT_CONNECTED}, ], - DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], + DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], @@ -154,7 +154,7 @@ {CONF_TYPE: CONF_OCCUPIED}, {CONF_TYPE: CONF_NOT_OCCUPIED}, ], - DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], @@ -166,7 +166,7 @@ {CONF_TYPE: CONF_VIBRATION}, {CONF_TYPE: CONF_NO_VIBRATION}, ], - DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], } diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 109a2b1fd45f61..e01af8d183ecb0 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -59,7 +59,7 @@ "no_light": "{entity_name} stopped detecting light", "locked": "{entity_name} locked", "not_locked": "{entity_name} unlocked", - "moist§": "{entity_name} became moist", + "moist": "{entity_name} became moist", "not_moist": "{entity_name} became dry", "motion": "{entity_name} started detecting motion", "no_motion": "{entity_name} stopped detecting motion", @@ -84,7 +84,7 @@ "vibration": "{entity_name} started detecting vibration", "no_vibration": "{entity_name} stopped detecting vibration", "opened": "{entity_name} opened", - "closed": "{entity_name} closed", + "not_opened": "{entity_name} closed", "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" From d4436951c5feab6d77f49e0e9f40e7e88170d333 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Oct 2019 11:19:39 -0700 Subject: [PATCH 291/296] Update translations --- .../components/.translations/airly.ca.json | 22 ++++++++ .../components/.translations/airly.da.json | 22 ++++++++ .../components/.translations/airly.de.json | 18 +++++++ .../components/.translations/airly.en.json | 22 ++++++++ .../components/.translations/airly.es.json | 22 ++++++++ .../components/.translations/airly.fr.json | 21 ++++++++ .../components/.translations/airly.it.json | 22 ++++++++ .../components/.translations/airly.lb.json | 22 ++++++++ .../components/.translations/airly.nn.json | 10 ++++ .../components/.translations/airly.no.json | 22 ++++++++ .../components/.translations/airly.pl.json | 22 ++++++++ .../components/.translations/airly.ru.json | 22 ++++++++ .../components/.translations/airly.sl.json | 22 ++++++++ .../.translations/airly.zh-Hant.json | 22 ++++++++ .../components/adguard/.translations/nn.json | 11 ++++ .../ambient_station/.translations/ru.json | 2 +- .../arcam_fmj/.translations/nn.json | 5 ++ .../components/axis/.translations/nn.json | 3 +- .../components/axis/.translations/ru.json | 4 +- .../binary_sensor/.translations/en.json | 2 + .../binary_sensor/.translations/fr.json | 54 +++++++++++++++++++ .../cert_expiry/.translations/ru.json | 4 +- .../components/daikin/.translations/nn.json | 5 ++ .../components/daikin/.translations/ru.json | 2 +- .../components/deconz/.translations/de.json | 10 ++++ .../components/deconz/.translations/es.json | 1 + .../components/deconz/.translations/fr.json | 1 + .../components/deconz/.translations/it.json | 1 + .../components/deconz/.translations/lb.json | 1 + .../components/deconz/.translations/no.json | 17 +++--- .../components/deconz/.translations/sl.json | 1 + .../deconz/.translations/zh-Hant.json | 1 + .../dialogflow/.translations/nn.json | 5 ++ .../components/ecobee/.translations/de.json | 11 ++++ .../components/ecobee/.translations/fr.json | 24 +++++++++ .../components/ecobee/.translations/nn.json | 5 ++ .../emulated_roku/.translations/nn.json | 5 ++ .../emulated_roku/.translations/ru.json | 2 +- .../components/esphome/.translations/nn.json | 7 ++- .../components/esphome/.translations/ru.json | 2 +- .../geonetnz_quakes/.translations/nn.json | 12 +++++ .../geonetnz_quakes/.translations/ru.json | 2 +- .../components/hangouts/.translations/ru.json | 2 +- .../components/heos/.translations/de.json | 2 +- .../homematicip_cloud/.translations/ru.json | 2 +- .../components/hue/.translations/ru.json | 4 +- .../iaqualink/.translations/nn.json | 5 ++ .../components/ipma/.translations/nn.json | 11 ++++ .../components/ipma/.translations/ru.json | 2 +- .../components/iqvia/.translations/nn.json | 10 ++++ .../components/iqvia/.translations/ru.json | 2 +- .../components/izone/.translations/nn.json | 10 ++++ .../components/life360/.translations/nn.json | 12 +++++ .../components/life360/.translations/ru.json | 4 +- .../components/lifx/.translations/nn.json | 10 ++++ .../components/light/.translations/de.json | 9 ++++ .../components/linky/.translations/nn.json | 10 ++++ .../components/linky/.translations/ru.json | 4 +- .../luftdaten/.translations/ru.json | 2 +- .../components/mailgun/.translations/nn.json | 5 ++ .../components/met/.translations/de.json | 2 +- .../components/met/.translations/nn.json | 11 ++++ .../components/met/.translations/pl.json | 2 +- .../components/met/.translations/ru.json | 2 +- .../components/neato/.translations/ca.json | 27 ++++++++++ .../components/neato/.translations/da.json | 23 ++++++++ .../components/neato/.translations/de.json | 27 ++++++++++ .../components/neato/.translations/en.json | 27 ++++++++++ .../components/neato/.translations/es.json | 27 ++++++++++ .../components/neato/.translations/fr.json | 27 ++++++++++ .../components/neato/.translations/it.json | 27 ++++++++++ .../components/neato/.translations/lb.json | 27 ++++++++++ .../components/neato/.translations/nn.json | 12 +++++ .../components/neato/.translations/no.json | 27 ++++++++++ .../components/neato/.translations/pl.json | 27 ++++++++++ .../components/neato/.translations/ru.json | 27 ++++++++++ .../components/neato/.translations/sl.json | 27 ++++++++++ .../neato/.translations/zh-Hant.json | 27 ++++++++++ .../components/notion/.translations/ru.json | 2 +- .../opentherm_gw/.translations/ca.json | 23 ++++++++ .../opentherm_gw/.translations/da.json | 20 +++++++ .../opentherm_gw/.translations/de.json | 19 +++++++ .../opentherm_gw/.translations/en.json | 23 ++++++++ .../opentherm_gw/.translations/es.json | 23 ++++++++ .../opentherm_gw/.translations/fr.json | 22 ++++++++ .../opentherm_gw/.translations/it.json | 23 ++++++++ .../opentherm_gw/.translations/lb.json | 23 ++++++++ .../opentherm_gw/.translations/nl.json | 14 +++++ .../opentherm_gw/.translations/nn.json | 12 +++++ .../opentherm_gw/.translations/no.json | 23 ++++++++ .../opentherm_gw/.translations/pl.json | 12 +++++ .../opentherm_gw/.translations/ru.json | 23 ++++++++ .../opentherm_gw/.translations/sl.json | 23 ++++++++ .../opentherm_gw/.translations/zh-Hant.json | 23 ++++++++ .../components/openuv/.translations/ru.json | 2 +- .../owntracks/.translations/nn.json | 5 ++ .../components/plaato/.translations/nn.json | 5 ++ .../components/plex/.translations/ca.json | 4 ++ .../components/plex/.translations/de.json | 26 +++++++++ .../components/plex/.translations/en.json | 5 ++ .../components/plex/.translations/es.json | 6 +++ .../components/plex/.translations/fr.json | 23 +++++++- .../components/plex/.translations/it.json | 8 ++- .../components/plex/.translations/lb.json | 6 +++ .../components/plex/.translations/nn.json | 5 ++ .../components/plex/.translations/no.json | 5 ++ .../components/plex/.translations/pl.json | 1 + .../components/plex/.translations/ru.json | 13 +++-- .../components/plex/.translations/sl.json | 6 +++ .../plex/.translations/zh-Hant.json | 8 ++- .../components/point/.translations/nn.json | 5 ++ .../components/ps4/.translations/nn.json | 13 ++++- .../rainmachine/.translations/pl.json | 2 +- .../rainmachine/.translations/ru.json | 2 +- .../components/sensor/.translations/ca.json | 24 +++++++++ .../components/sensor/.translations/da.json | 26 +++++++++ .../components/sensor/.translations/de.json | 21 ++++++++ .../components/sensor/.translations/en.json | 26 +++++++++ .../components/sensor/.translations/es.json | 26 +++++++++ .../components/sensor/.translations/fr.json | 26 +++++++++ .../components/sensor/.translations/it.json | 26 +++++++++ .../components/sensor/.translations/lb.json | 26 +++++++++ .../components/sensor/.translations/no.json | 26 +++++++++ .../components/sensor/.translations/pl.json | 9 ++++ .../components/sensor/.translations/sl.json | 26 +++++++++ .../sensor/.translations/zh-Hant.json | 26 +++++++++ .../simplisafe/.translations/nn.json | 5 ++ .../simplisafe/.translations/ru.json | 2 +- .../components/smhi/.translations/ru.json | 2 +- .../solaredge/.translations/ru.json | 4 +- .../components/soma/.translations/de.json | 9 ++++ .../components/soma/.translations/es.json | 13 +++++ .../components/soma/.translations/fr.json | 13 +++++ .../components/soma/.translations/lb.json | 13 +++++ .../components/soma/.translations/nn.json | 5 ++ .../components/soma/.translations/pl.json | 13 +++++ .../soma/.translations/zh-Hant.json | 13 +++++ .../components/somfy/.translations/nn.json | 5 ++ .../tellduslive/.translations/nn.json | 5 ++ .../tellduslive/.translations/ru.json | 2 +- .../components/toon/.translations/nn.json | 7 +++ .../components/tplink/.translations/nn.json | 10 ++++ .../components/traccar/.translations/nn.json | 5 ++ .../transmission/.translations/de.json | 37 +++++++++++++ .../transmission/.translations/en.json | 2 +- .../transmission/.translations/fr.json | 40 ++++++++++++++ .../transmission/.translations/nn.json | 12 +++++ .../transmission/.translations/ru.json | 2 +- .../twentemilieu/.translations/nn.json | 10 ++++ .../components/twilio/.translations/nn.json | 5 ++ .../components/unifi/.translations/ca.json | 5 ++ .../components/unifi/.translations/da.json | 5 ++ .../components/unifi/.translations/de.json | 5 ++ .../components/unifi/.translations/en.json | 5 ++ .../components/unifi/.translations/es.json | 5 ++ .../components/unifi/.translations/fr.json | 5 ++ .../components/unifi/.translations/it.json | 5 ++ .../components/unifi/.translations/lb.json | 5 ++ .../components/unifi/.translations/nn.json | 11 ++++ .../components/unifi/.translations/no.json | 5 ++ .../components/unifi/.translations/pl.json | 5 ++ .../components/unifi/.translations/ru.json | 7 ++- .../components/unifi/.translations/sl.json | 5 ++ .../unifi/.translations/zh-Hant.json | 5 ++ .../components/upnp/.translations/nn.json | 3 +- .../components/upnp/.translations/ru.json | 2 +- .../components/vesync/.translations/nn.json | 5 ++ .../components/wemo/.translations/nn.json | 10 ++++ .../components/withings/.translations/nn.json | 5 ++ .../components/wwlln/.translations/ru.json | 2 +- .../components/zha/.translations/da.json | 3 ++ .../components/zha/.translations/de.json | 8 +++ .../components/zha/.translations/fr.json | 27 ++++++++++ .../components/zha/.translations/nn.json | 10 ++++ .../components/zha/.translations/no.json | 12 ++--- .../components/zone/.translations/ru.json | 2 +- .../components/zwave/.translations/nn.json | 3 +- .../components/zwave/.translations/ru.json | 2 +- 178 files changed, 2058 insertions(+), 67 deletions(-) create mode 100644 homeassistant/components/.translations/airly.ca.json create mode 100644 homeassistant/components/.translations/airly.da.json create mode 100644 homeassistant/components/.translations/airly.de.json create mode 100644 homeassistant/components/.translations/airly.en.json create mode 100644 homeassistant/components/.translations/airly.es.json create mode 100644 homeassistant/components/.translations/airly.fr.json create mode 100644 homeassistant/components/.translations/airly.it.json create mode 100644 homeassistant/components/.translations/airly.lb.json create mode 100644 homeassistant/components/.translations/airly.nn.json create mode 100644 homeassistant/components/.translations/airly.no.json create mode 100644 homeassistant/components/.translations/airly.pl.json create mode 100644 homeassistant/components/.translations/airly.ru.json create mode 100644 homeassistant/components/.translations/airly.sl.json create mode 100644 homeassistant/components/.translations/airly.zh-Hant.json create mode 100644 homeassistant/components/adguard/.translations/nn.json create mode 100644 homeassistant/components/arcam_fmj/.translations/nn.json create mode 100644 homeassistant/components/binary_sensor/.translations/fr.json create mode 100644 homeassistant/components/daikin/.translations/nn.json create mode 100644 homeassistant/components/dialogflow/.translations/nn.json create mode 100644 homeassistant/components/ecobee/.translations/de.json create mode 100644 homeassistant/components/ecobee/.translations/fr.json create mode 100644 homeassistant/components/ecobee/.translations/nn.json create mode 100644 homeassistant/components/emulated_roku/.translations/nn.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/nn.json create mode 100644 homeassistant/components/iaqualink/.translations/nn.json create mode 100644 homeassistant/components/ipma/.translations/nn.json create mode 100644 homeassistant/components/iqvia/.translations/nn.json create mode 100644 homeassistant/components/izone/.translations/nn.json create mode 100644 homeassistant/components/life360/.translations/nn.json create mode 100644 homeassistant/components/lifx/.translations/nn.json create mode 100644 homeassistant/components/linky/.translations/nn.json create mode 100644 homeassistant/components/mailgun/.translations/nn.json create mode 100644 homeassistant/components/met/.translations/nn.json create mode 100644 homeassistant/components/neato/.translations/ca.json create mode 100644 homeassistant/components/neato/.translations/da.json create mode 100644 homeassistant/components/neato/.translations/de.json create mode 100644 homeassistant/components/neato/.translations/en.json create mode 100644 homeassistant/components/neato/.translations/es.json create mode 100644 homeassistant/components/neato/.translations/fr.json create mode 100644 homeassistant/components/neato/.translations/it.json create mode 100644 homeassistant/components/neato/.translations/lb.json create mode 100644 homeassistant/components/neato/.translations/nn.json create mode 100644 homeassistant/components/neato/.translations/no.json create mode 100644 homeassistant/components/neato/.translations/pl.json create mode 100644 homeassistant/components/neato/.translations/ru.json create mode 100644 homeassistant/components/neato/.translations/sl.json create mode 100644 homeassistant/components/neato/.translations/zh-Hant.json create mode 100644 homeassistant/components/opentherm_gw/.translations/ca.json create mode 100644 homeassistant/components/opentherm_gw/.translations/da.json create mode 100644 homeassistant/components/opentherm_gw/.translations/de.json create mode 100644 homeassistant/components/opentherm_gw/.translations/en.json create mode 100644 homeassistant/components/opentherm_gw/.translations/es.json create mode 100644 homeassistant/components/opentherm_gw/.translations/fr.json create mode 100644 homeassistant/components/opentherm_gw/.translations/it.json create mode 100644 homeassistant/components/opentherm_gw/.translations/lb.json create mode 100644 homeassistant/components/opentherm_gw/.translations/nl.json create mode 100644 homeassistant/components/opentherm_gw/.translations/nn.json create mode 100644 homeassistant/components/opentherm_gw/.translations/no.json create mode 100644 homeassistant/components/opentherm_gw/.translations/pl.json create mode 100644 homeassistant/components/opentherm_gw/.translations/ru.json create mode 100644 homeassistant/components/opentherm_gw/.translations/sl.json create mode 100644 homeassistant/components/opentherm_gw/.translations/zh-Hant.json create mode 100644 homeassistant/components/owntracks/.translations/nn.json create mode 100644 homeassistant/components/plaato/.translations/nn.json create mode 100644 homeassistant/components/plex/.translations/de.json create mode 100644 homeassistant/components/plex/.translations/nn.json create mode 100644 homeassistant/components/point/.translations/nn.json create mode 100644 homeassistant/components/sensor/.translations/ca.json create mode 100644 homeassistant/components/sensor/.translations/da.json create mode 100644 homeassistant/components/sensor/.translations/de.json create mode 100644 homeassistant/components/sensor/.translations/en.json create mode 100644 homeassistant/components/sensor/.translations/es.json create mode 100644 homeassistant/components/sensor/.translations/fr.json create mode 100644 homeassistant/components/sensor/.translations/it.json create mode 100644 homeassistant/components/sensor/.translations/lb.json create mode 100644 homeassistant/components/sensor/.translations/no.json create mode 100644 homeassistant/components/sensor/.translations/pl.json create mode 100644 homeassistant/components/sensor/.translations/sl.json create mode 100644 homeassistant/components/sensor/.translations/zh-Hant.json create mode 100644 homeassistant/components/simplisafe/.translations/nn.json create mode 100644 homeassistant/components/soma/.translations/de.json create mode 100644 homeassistant/components/soma/.translations/es.json create mode 100644 homeassistant/components/soma/.translations/fr.json create mode 100644 homeassistant/components/soma/.translations/lb.json create mode 100644 homeassistant/components/soma/.translations/nn.json create mode 100644 homeassistant/components/soma/.translations/pl.json create mode 100644 homeassistant/components/soma/.translations/zh-Hant.json create mode 100644 homeassistant/components/somfy/.translations/nn.json create mode 100644 homeassistant/components/tellduslive/.translations/nn.json create mode 100644 homeassistant/components/tplink/.translations/nn.json create mode 100644 homeassistant/components/traccar/.translations/nn.json create mode 100644 homeassistant/components/transmission/.translations/de.json create mode 100644 homeassistant/components/transmission/.translations/fr.json create mode 100644 homeassistant/components/transmission/.translations/nn.json create mode 100644 homeassistant/components/twentemilieu/.translations/nn.json create mode 100644 homeassistant/components/twilio/.translations/nn.json create mode 100644 homeassistant/components/unifi/.translations/nn.json create mode 100644 homeassistant/components/vesync/.translations/nn.json create mode 100644 homeassistant/components/wemo/.translations/nn.json create mode 100644 homeassistant/components/withings/.translations/nn.json create mode 100644 homeassistant/components/zha/.translations/nn.json diff --git a/homeassistant/components/.translations/airly.ca.json b/homeassistant/components/.translations/airly.ca.json new file mode 100644 index 00000000000000..bf50b4f23e55b5 --- /dev/null +++ b/homeassistant/components/.translations/airly.ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La clau API no \u00e9s correcta.", + "name_exists": "El nom ja existeix.", + "wrong_location": "No hi ha estacions de mesura Airly en aquesta zona." + }, + "step": { + "user": { + "data": { + "api_key": "Clau API d'Airly", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom de la integraci\u00f3" + }, + "description": "Configura una integraci\u00f3 de qualitat d\u2019aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.da.json b/homeassistant/components/.translations/airly.da.json new file mode 100644 index 00000000000000..652cc46a7b3e0a --- /dev/null +++ b/homeassistant/components/.translations/airly.da.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-n\u00f8glen er ikke korrekt.", + "name_exists": "Navnet findes allerede.", + "wrong_location": "Ingen Airly m\u00e5lestationer i dette omr\u00e5de." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-n\u00f8gle", + "latitude": "Breddegrad", + "longitude": "L\u00e6ngdegrad", + "name": "Integrationens navn" + }, + "description": "Konfigurer Airly luftkvalitet integration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.de.json b/homeassistant/components/.translations/airly.de.json new file mode 100644 index 00000000000000..cb290dc46c087e --- /dev/null +++ b/homeassistant/components/.translations/airly.de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "Name existiert bereits" + }, + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name der Integration" + }, + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.en.json b/homeassistant/components/.translations/airly.en.json new file mode 100644 index 00000000000000..83284aaeb7b6e0 --- /dev/null +++ b/homeassistant/components/.translations/airly.en.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API key is not correct.", + "name_exists": "Name already exists.", + "wrong_location": "No Airly measuring stations in this area." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name of the integration" + }, + "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.es.json b/homeassistant/components/.translations/airly.es.json new file mode 100644 index 00000000000000..0c29ad0bc668b9 --- /dev/null +++ b/homeassistant/components/.translations/airly.es.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La clave de la API no es correcta.", + "name_exists": "El nombre ya existe.", + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta zona." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API de Airly", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre de la integraci\u00f3n" + }, + "description": "Establecer la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave de la API vaya a https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.fr.json b/homeassistant/components/.translations/airly.fr.json new file mode 100644 index 00000000000000..cf756a9f4928b9 --- /dev/null +++ b/homeassistant/components/.translations/airly.fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "auth": "La cl\u00e9 API n'est pas correcte.", + "name_exists": "Le nom existe d\u00e9j\u00e0.", + "wrong_location": "Aucune station de mesure Airly dans cette zone." + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 API Airly", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom de l'int\u00e9gration" + }, + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.it.json b/homeassistant/components/.translations/airly.it.json new file mode 100644 index 00000000000000..e50f618575bfd6 --- /dev/null +++ b/homeassistant/components/.translations/airly.it.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La chiave API non \u00e8 corretta.", + "name_exists": "Il nome \u00e8 gi\u00e0 esistente", + "wrong_location": "Nessuna stazione di misurazione Airly in quest'area." + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API Airly", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome dell'integrazione" + }, + "description": "Configurazione dell'integrazione della qualit\u00e0 dell'aria Airly. Per generare la chiave API andare su https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.lb.json b/homeassistant/components/.translations/airly.lb.json new file mode 100644 index 00000000000000..08aac57d162ae9 --- /dev/null +++ b/homeassistant/components/.translations/airly.lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Api Schl\u00ebssel ass net korrekt.", + "name_exists": "Numm g\u00ebtt et schonn", + "wrong_location": "Keng Airly Moos Statioun an d\u00ebsem Ber\u00e4ich" + }, + "step": { + "user": { + "data": { + "api_key": "Airly API Schl\u00ebssel", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm vun der Installatioun" + }, + "description": "Airly Loft Qualit\u00e9it Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle gitt op https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.nn.json b/homeassistant/components/.translations/airly.nn.json new file mode 100644 index 00000000000000..7e2f4f1ff6bcef --- /dev/null +++ b/homeassistant/components/.translations/airly.nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.no.json b/homeassistant/components/.translations/airly.no.json new file mode 100644 index 00000000000000..70924bb7bf4b9e --- /dev/null +++ b/homeassistant/components/.translations/airly.no.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-n\u00f8kkelen er ikke korrekt.", + "name_exists": "Navnet finnes allerede.", + "wrong_location": "Ingen Airly m\u00e5lestasjoner i dette omr\u00e5det." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn p\u00e5 integrasjonen" + }, + "description": "Sett opp Airly luftkvalitet integrering. For \u00e5 generere API-n\u00f8kkel g\u00e5 til https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.pl.json b/homeassistant/components/.translations/airly.pl.json new file mode 100644 index 00000000000000..5d601b37591b68 --- /dev/null +++ b/homeassistant/components/.translations/airly.pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Klucz API jest nieprawid\u0142owy.", + "name_exists": "Nazwa ju\u017c istnieje.", + "wrong_location": "Brak stacji pomiarowych Airly w tym rejonie." + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API Airly", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa integracji" + }, + "description": "Konfiguracja integracji Airly. By wygenerowa\u0107 klucz API, przejd\u017a na stron\u0119 https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.ru.json b/homeassistant/components/.translations/airly.ru.json new file mode 100644 index 00000000000000..36080c9f372e24 --- /dev/null +++ b/homeassistant/components/.translations/airly.ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", + "wrong_location": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 Airly." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 Airly. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://developer.airly.eu/register.", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.sl.json b/homeassistant/components/.translations/airly.sl.json new file mode 100644 index 00000000000000..08f57d88bcba98 --- /dev/null +++ b/homeassistant/components/.translations/airly.sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Klju\u010d API ni pravilen.", + "name_exists": "Ime \u017ee obstaja", + "wrong_location": "Na tem obmo\u010dju ni merilnih postaj Airly." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API klju\u010d", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime integracije" + }, + "description": "Nastavite Airly integracijo za kakovost zraka. \u010ce \u017eelite ustvariti API klju\u010d pojdite na https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.zh-Hant.json b/homeassistant/components/.translations/airly.zh-Hant.json new file mode 100644 index 00000000000000..bb38d2b9b8c739 --- /dev/null +++ b/homeassistant/components/.translations/airly.zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API \u5bc6\u9470\u4e0d\u6b63\u78ba\u3002", + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", + "wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002" + }, + "step": { + "user": { + "data": { + "api_key": "Airly API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u6574\u5408\u540d\u7a31" + }, + "description": "\u6b32\u8a2d\u5b9a Airly \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://developer.airly.eu/register \u7522\u751f API \u5bc6\u9470", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/nn.json b/homeassistant/components/adguard/.translations/nn.json new file mode 100644 index 00000000000000..7c129cba3afc71 --- /dev/null +++ b/homeassistant/components/adguard/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json index d1264010b75c1c..2d7964f18ebe61 100644 --- a/homeassistant/components/ambient_station/.translations/ru.json +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d", + "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b" }, diff --git a/homeassistant/components/arcam_fmj/.translations/nn.json b/homeassistant/components/arcam_fmj/.translations/nn.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/nn.json b/homeassistant/components/axis/.translations/nn.json index 3364446935953a..b6296d1acab703 100644 --- a/homeassistant/components/axis/.translations/nn.json +++ b/homeassistant/components/axis/.translations/nn.json @@ -5,7 +5,8 @@ "data": { "host": "Vert", "password": "Passord", - "port": "Port" + "port": "Port", + "username": "Brukarnamn" } } } diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 67d720aa85f06a..951263d53f9668 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis" }, "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e", "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" diff --git a/homeassistant/components/binary_sensor/.translations/en.json b/homeassistant/components/binary_sensor/.translations/en.json index 6379df936b898d..93b61893980eb6 100644 --- a/homeassistant/components/binary_sensor/.translations/en.json +++ b/homeassistant/components/binary_sensor/.translations/en.json @@ -53,6 +53,7 @@ "hot": "{entity_name} became hot", "light": "{entity_name} started detecting light", "locked": "{entity_name} locked", + "moist": "{entity_name} became moist", "moist\u00a7": "{entity_name} became moist", "motion": "{entity_name} started detecting motion", "moving": "{entity_name} started moving", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} became dry", "not_moving": "{entity_name} stopped moving", "not_occupied": "{entity_name} became not occupied", + "not_opened": "{entity_name} closed", "not_plugged_in": "{entity_name} unplugged", "not_powered": "{entity_name} not powered", "not_present": "{entity_name} not present", diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json new file mode 100644 index 00000000000000..80792f166354ec --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -0,0 +1,54 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batterie faible", + "is_cold": "{entity_name} est froid", + "is_connected": "{entity_name} est connect\u00e9", + "is_gas": "{entity_name} d\u00e9tecte du gaz", + "is_hot": "{entity_name} est chaud", + "is_light": "{entity_name} d\u00e9tecte de la lumi\u00e8re", + "is_locked": "{entity_name} est verrouill\u00e9", + "is_moist": "{entity_name} est humide", + "is_motion": "{entity_name} d\u00e9tecte un mouvement", + "is_moving": "{entity_name} se d\u00e9place", + "is_no_gas": "{entity_name} ne d\u00e9tecte pas de gaz", + "is_no_light": "{entity_name} ne d\u00e9tecte pas de lumi\u00e8re", + "is_no_motion": "{entity_name} ne d\u00e9tecte pas de mouvement", + "is_no_problem": "{entity_name} ne d\u00e9tecte pas de probl\u00e8me", + "is_no_smoke": "{entity_name} ne d\u00e9tecte pas de fum\u00e9e", + "is_no_sound": "{entity_name} ne d\u00e9tecte pas de son", + "is_no_vibration": "{entity_name} ne d\u00e9tecte pas de vibration", + "is_not_bat_low": "{entity_name} batterie normale", + "is_not_cold": "{entity_name} n'est pas froid", + "is_not_connected": "{entity_name} est d\u00e9connect\u00e9", + "is_not_hot": "{entity_name} n'est pas chaud", + "is_not_locked": "{entity_name} est d\u00e9verrouill\u00e9", + "is_not_moist": "{entity_name} est sec", + "is_not_moving": "{entity_name} ne bouge pas", + "is_not_occupied": "{entity_name} n'est pas occup\u00e9", + "is_not_open": "{entity_name} est ferm\u00e9", + "is_not_plugged_in": "{entity_name} est d\u00e9branch\u00e9", + "is_not_powered": "{entity_name} n'est pas aliment\u00e9", + "is_not_present": "{entity_name} n'est pas pr\u00e9sent", + "is_not_unsafe": "{entity_name} est en s\u00e9curit\u00e9", + "is_occupied": "{entity_name} est occup\u00e9", + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9", + "is_open": "{entity_name} est ouvert", + "is_plugged_in": "{entity_name} est branch\u00e9", + "is_powered": "{entity_name} est aliment\u00e9", + "is_present": "{entity_name} est pr\u00e9sent", + "is_problem": "{entity_name} d\u00e9tecte un probl\u00e8me", + "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", + "is_sound": "{entity_name} d\u00e9tecte du son" + }, + "trigger_type": { + "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", + "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", + "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} activ\u00e9", + "unsafe": "{entity_name} est devenu dangereux", + "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json index 6a795dee13e26b..d962c7931218c7 100644 --- a/homeassistant/components/cert_expiry/.translations/ru.json +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430" + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." }, "error": { "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430", "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443", - "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430", + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442" }, "step": { diff --git a/homeassistant/components/daikin/.translations/nn.json b/homeassistant/components/daikin/.translations/nn.json new file mode 100644 index 00000000000000..67d4f85262572e --- /dev/null +++ b/homeassistant/components/daikin/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index ce1f1ab3caa974..98ab98e6b170d2 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 97e25e28965c5f..830ae0fd13f2f1 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -41,6 +41,16 @@ }, "title": "deCONZ Zigbee Gateway" }, + "device_automation": { + "trigger_subtype": { + "close": "Schlie\u00dfen", + "left": "Links", + "open": "Offen", + "right": "Rechts", + "turn_off": "Ausschalten", + "turn_on": "Einschalten" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index cb5db0b8348ea4..04a08d185b30fb 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", + "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtipo}\" detenido", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index cc6d22945dcb79..3729f7f556afb9 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Bouton \"{subtype}\" quadruple cliqu\u00e9", "remote_button_quintuple_press": "Bouton \"{subtype}\" quintuple cliqu\u00e9", "remote_button_rotated": "Bouton \"{subtype}\" tourn\u00e9", + "remote_button_rotation_stopped": "La rotation du bouton \" {subtype} \" s'est arr\u00eat\u00e9e", "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 7a2b8832864e2b..1f0b344a32d6ba 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", "remote_button_rotated": "Pulsante ruotato \"{subtype}\"", + "remote_button_rotation_stopped": "La rotazione dei pulsanti \"{subtype}\" si \u00e8 arrestata", "remote_button_short_press": "Pulsante \"{subtype}\" premuto", "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 840bc8929a7366..f5f41a28a32c95 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt", "remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt", "remote_button_rotated": "Kn\u00e4ppche gedr\u00e9int \"{subtype}\"", + "remote_button_rotation_stopped": "Kn\u00e4ppchen Rotatioun \"{subtype}\" gestoppt", "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index c7079fd62193e7..7d05a366cf2248 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -58,15 +58,16 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { - "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", - "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", - "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\" {subtype} \" - knappen femdobbelt klikket", - "remote_button_rotated": "Knappen roterte \" {subtype} \"", - "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", + "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt klikket", + "remote_button_rotated": "Knappen roterte \"{subtype}\"", + "remote_button_rotation_stopped": "Knappe rotasjon \"{subtype}\" stoppet", + "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", - "remote_button_triple_press": "\" {subtype} \"-knappen trippel klikket", + "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", "remote_gyro_activated": "Enhet er ristet" } }, diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 9aebb2a556f6cd..0717bcfc39f455 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen", "remote_button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen", "remote_button_rotated": "Gumb \"{subtype}\" zasukan", + "remote_button_rotation_stopped": "Vrtenje \"{subtype}\" gumba se je ustavilo", "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index bd47a637761ccd..2ad613cde6868f 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u9ede\u64ca", "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u9ede\u64ca", "remote_button_rotated": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215", + "remote_button_rotation_stopped": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215\u5df2\u505c\u6b62", "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", diff --git a/homeassistant/components/dialogflow/.translations/nn.json b/homeassistant/components/dialogflow/.translations/nn.json new file mode 100644 index 00000000000000..5a96b853eb0944 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/de.json b/homeassistant/components/ecobee/.translations/de.json new file mode 100644 index 00000000000000..1959f769d3a4c9 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/de.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/fr.json b/homeassistant/components/ecobee/.translations/fr.json new file mode 100644 index 00000000000000..85da5b3a4ecc41 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "one_instance_only": "Cette int\u00e9gration ne prend actuellement en charge qu'une seule instance ecobee." + }, + "error": { + "pin_request_failed": "Erreur lors de la demande du code PIN \u00e0 ecobee; veuillez v\u00e9rifier que la cl\u00e9 API est correcte.", + "token_request_failed": "Erreur lors de la demande de jetons \u00e0 ecobee; Veuillez r\u00e9essayer." + }, + "step": { + "authorize": { + "title": "Autoriser l'application sur ecobee.com" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 API" + }, + "description": "Veuillez entrer la cl\u00e9 API obtenue aupr\u00e8s d'ecobee.com.", + "title": "Cl\u00e9 API ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/nn.json b/homeassistant/components/ecobee/.translations/nn.json new file mode 100644 index 00000000000000..301239cf31a6c9 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/nn.json b/homeassistant/components/emulated_roku/.translations/nn.json new file mode 100644 index 00000000000000..fc349a0d9de9df --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/ru.json b/homeassistant/components/emulated_roku/.translations/ru.json index c7b85c195929d3..32bf473ac38546 100644 --- a/homeassistant/components/emulated_roku/.translations/ru.json +++ b/homeassistant/components/emulated_roku/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/esphome/.translations/nn.json b/homeassistant/components/esphome/.translations/nn.json index 830391f58f6e30..5e40c8ec5e5635 100644 --- a/homeassistant/components/esphome/.translations/nn.json +++ b/homeassistant/components/esphome/.translations/nn.json @@ -1,9 +1,14 @@ { "config": { + "flow_title": "ESPHome: {name}", "step": { "discovery_confirm": { "title": "Fann ESPhome node" + }, + "user": { + "title": "ESPHome" } - } + }, + "title": "ESPHome" } } \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json index 1405112c07022f..62d24662ab6a77 100644 --- a/homeassistant/components/esphome/.translations/ru.json +++ b/homeassistant/components/esphome/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430" + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", diff --git a/homeassistant/components/geonetnz_quakes/.translations/nn.json b/homeassistant/components/geonetnz_quakes/.translations/nn.json new file mode 100644 index 00000000000000..d8afb1e7aaead1 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/ru.json b/homeassistant/components/geonetnz_quakes/.translations/ru.json index 7d6583bc1d5b59..d6763d17e2d0a8 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ru.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index 52b8798c0f4084..6942f683fa6e4a 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" }, "error": { diff --git a/homeassistant/components/heos/.translations/de.json b/homeassistant/components/heos/.translations/de.json index e8f4df930dbe99..e98df7466ff69c 100644 --- a/homeassistant/components/heos/.translations/de.json +++ b/homeassistant/components/heos/.translations/de.json @@ -16,6 +16,6 @@ "title": "Mit Heos verbinden" } }, - "title": "Heos" + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 82ecd4a32504f2..5155a42c4c3b55 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP", "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index be5d2b7159d40b..79a46e1861bce6 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", diff --git a/homeassistant/components/iaqualink/.translations/nn.json b/homeassistant/components/iaqualink/.translations/nn.json new file mode 100644 index 00000000000000..ea78f0d0d5dd0c --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/nn.json b/homeassistant/components/ipma/.translations/nn.json new file mode 100644 index 00000000000000..0e024a0e1eb7b0 --- /dev/null +++ b/homeassistant/components/ipma/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json index a260efa5bd9dc2..a302572ed121d5 100644 --- a/homeassistant/components/ipma/.translations/ru.json +++ b/homeassistant/components/ipma/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/iqvia/.translations/nn.json b/homeassistant/components/iqvia/.translations/nn.json new file mode 100644 index 00000000000000..89922b66f03dca --- /dev/null +++ b/homeassistant/components/iqvia/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/ru.json b/homeassistant/components/iqvia/.translations/ru.json index 06a5b7e69ddde5..0c3afc88c94dd0 100644 --- a/homeassistant/components/iqvia/.translations/ru.json +++ b/homeassistant/components/iqvia/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d", + "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" }, "step": { diff --git a/homeassistant/components/izone/.translations/nn.json b/homeassistant/components/izone/.translations/nn.json new file mode 100644 index 00000000000000..eaf7601be9c3d8 --- /dev/null +++ b/homeassistant/components/izone/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/nn.json b/homeassistant/components/life360/.translations/nn.json new file mode 100644 index 00000000000000..98345b022f2e1b --- /dev/null +++ b/homeassistant/components/life360/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + }, + "title": "Life360" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index 1e962142373f89..d033da4bae71eb 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "create_entry": { "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." @@ -11,7 +11,7 @@ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d", "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "step": { "user": { diff --git a/homeassistant/components/lifx/.translations/nn.json b/homeassistant/components/lifx/.translations/nn.json new file mode 100644 index 00000000000000..c78905b09c8f95 --- /dev/null +++ b/homeassistant/components/lifx/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json index e07adeb0a36e84..be8966d95569af 100644 --- a/homeassistant/components/light/.translations/de.json +++ b/homeassistant/components/light/.translations/de.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Schalte {entity_name} um.", + "turn_off": "Schalte {entity_name} aus.", + "turn_on": "Schalte {entity_name} ein." + }, + "condition_type": { + "is_off": "{entity_name} ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet" + }, "trigger_type": { "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet" diff --git a/homeassistant/components/linky/.translations/nn.json b/homeassistant/components/linky/.translations/nn.json new file mode 100644 index 00000000000000..6e084d1a9d28fc --- /dev/null +++ b/homeassistant/components/linky/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index 498b5b2f12f29b..b569cce9239066 100644 --- a/homeassistant/components/linky/.translations/ru.json +++ b/homeassistant/components/linky/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "error": { "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443", "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430", + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c" }, "step": { diff --git a/homeassistant/components/luftdaten/.translations/ru.json b/homeassistant/components/luftdaten/.translations/ru.json index d37aa3567d1977..7ae83b550e3d44 100644 --- a/homeassistant/components/luftdaten/.translations/ru.json +++ b/homeassistant/components/luftdaten/.translations/ru.json @@ -3,7 +3,7 @@ "error": { "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten", "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d", - "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/.translations/nn.json b/homeassistant/components/mailgun/.translations/nn.json new file mode 100644 index 00000000000000..2bab2e430016cf --- /dev/null +++ b/homeassistant/components/mailgun/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/de.json b/homeassistant/components/met/.translations/de.json index b70d3f12a838ce..2fd772c8619bdd 100644 --- a/homeassistant/components/met/.translations/de.json +++ b/homeassistant/components/met/.translations/de.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Name existiert bereits" + "name_exists": "Ort existiert bereits" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/nn.json b/homeassistant/components/met/.translations/nn.json new file mode 100644 index 00000000000000..0e024a0e1eb7b0 --- /dev/null +++ b/homeassistant/components/met/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/pl.json b/homeassistant/components/met/.translations/pl.json index d44142213bf066..f647dcf7b45365 100644 --- a/homeassistant/components/met/.translations/pl.json +++ b/homeassistant/components/met/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Nazwa ju\u017c istnieje" + "name_exists": "Lokalizacja ju\u017c istnieje" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/ru.json b/homeassistant/components/met/.translations/ru.json index d92d28d948419d..559382cf209d67 100644 --- a/homeassistant/components/met/.translations/ru.json +++ b/homeassistant/components/met/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/ca.json b/homeassistant/components/neato/.translations/ca.json new file mode 100644 index 00000000000000..d30f9e5ad4bd7b --- /dev/null +++ b/homeassistant/components/neato/.translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ja configurat", + "invalid_credentials": "Credencials inv\u00e0lides" + }, + "create_entry": { + "default": "Consulta la [documentaci\u00f3 de Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credencials inv\u00e0lides", + "unexpected_error": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari", + "vendor": "Venedor" + }, + "description": "Consulta la [documentaci\u00f3 de Neato]({docs_url}).", + "title": "Informaci\u00f3 del compte Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json new file mode 100644 index 00000000000000..7f0d122f38b2a7 --- /dev/null +++ b/homeassistant/components/neato/.translations/da.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Allerede konfigureret", + "invalid_credentials": "Ugyldige legitimationsoplysninger" + }, + "error": { + "invalid_credentials": "Ugyldige legitimationsoplysninger", + "unexpected_error": "Uventet fejl" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + }, + "description": "Se [Neato-dokumentation] ({docs_url}).", + "title": "Neato kontooplysninger" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/de.json b/homeassistant/components/neato/.translations/de.json new file mode 100644 index 00000000000000..2a75d11a9ec0c4 --- /dev/null +++ b/homeassistant/components/neato/.translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Bereits konfiguriert", + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + }, + "create_entry": { + "default": "Siehe [Neato-Dokumentation]({docs_url})." + }, + "error": { + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen", + "unexpected_error": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername", + "vendor": "Hersteller" + }, + "description": "Siehe [Neato-Dokumentation]({docs_url}).", + "title": "Neato-Kontoinformationen" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/en.json b/homeassistant/components/neato/.translations/en.json new file mode 100644 index 00000000000000..73628c8646e849 --- /dev/null +++ b/homeassistant/components/neato/.translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Already configured", + "invalid_credentials": "Invalid credentials" + }, + "create_entry": { + "default": "See [Neato documentation]({docs_url})." + }, + "error": { + "invalid_credentials": "Invalid credentials", + "unexpected_error": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username", + "vendor": "Vendor" + }, + "description": "See [Neato documentation]({docs_url}).", + "title": "Neato Account Info" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/es.json b/homeassistant/components/neato/.translations/es.json new file mode 100644 index 00000000000000..99e7574e4b2743 --- /dev/null +++ b/homeassistant/components/neato/.translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ya est\u00e1 configurado", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "create_entry": { + "default": "Ver [documentaci\u00f3n Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credenciales no v\u00e1lidas", + "unexpected_error": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario", + "vendor": "Vendedor" + }, + "description": "Ver [documentaci\u00f3n Neato]({docs_url}).", + "title": "Informaci\u00f3n de la cuenta de Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/fr.json b/homeassistant/components/neato/.translations/fr.json new file mode 100644 index 00000000000000..941ed18660efff --- /dev/null +++ b/homeassistant/components/neato/.translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00e9j\u00e0 configur\u00e9", + "invalid_credentials": "Informations d'identification invalides" + }, + "create_entry": { + "default": "Voir [Documentation Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Informations d'identification invalides", + "unexpected_error": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur", + "vendor": "Vendeur" + }, + "description": "Voir [Documentation Neato] ( {docs_url} ).", + "title": "Informations compte Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/it.json b/homeassistant/components/neato/.translations/it.json new file mode 100644 index 00000000000000..d5615815dc769b --- /dev/null +++ b/homeassistant/components/neato/.translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Gi\u00e0 configurato", + "invalid_credentials": "Credenziali non valide" + }, + "create_entry": { + "default": "Vedere la [Documentazione di Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credenziali non valide", + "unexpected_error": "Errore inaspettato" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente", + "vendor": "Fornitore" + }, + "description": "Vedere la [Documentazione di Neato]({docs_url}).", + "title": "Informazioni sull'account Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/lb.json b/homeassistant/components/neato/.translations/lb.json new file mode 100644 index 00000000000000..3043ec6ec3754c --- /dev/null +++ b/homeassistant/components/neato/.translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Scho konfigur\u00e9iert", + "invalid_credentials": "Ong\u00eblteg Login Informatioune" + }, + "create_entry": { + "default": "Kuckt [Neato Dokumentatioun]({docs_url})." + }, + "error": { + "invalid_credentials": "Ong\u00eblteg Login Informatioune", + "unexpected_error": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm", + "vendor": "Hiersteller" + }, + "description": "Kuckt [Neato Dokumentatioun]({docs_url}).", + "title": "Neato Kont Informatiounen" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/nn.json b/homeassistant/components/neato/.translations/nn.json new file mode 100644 index 00000000000000..e04e73aef24ea3 --- /dev/null +++ b/homeassistant/components/neato/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/no.json b/homeassistant/components/neato/.translations/no.json new file mode 100644 index 00000000000000..084c4b50e457ed --- /dev/null +++ b/homeassistant/components/neato/.translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Allerede konfigurert", + "invalid_credentials": "Ugyldig brukerinformasjon" + }, + "create_entry": { + "default": "Se [Neato dokumentasjon]({docs_url})." + }, + "error": { + "invalid_credentials": "Ugyldig brukerinformasjon", + "unexpected_error": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn", + "vendor": "Leverand\u00f8r" + }, + "description": "Se [Neato dokumentasjon]({docs_url}).", + "title": "Neato kontoinformasjon" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/pl.json b/homeassistant/components/neato/.translations/pl.json new file mode 100644 index 00000000000000..caea115b7d5101 --- /dev/null +++ b/homeassistant/components/neato/.translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" + }, + "create_entry": { + "default": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", + "unexpected_error": "Niespodziewany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika", + "vendor": "Dostawca" + }, + "description": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url}).", + "title": "Informacje o koncie Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json new file mode 100644 index 00000000000000..1a206258e24979 --- /dev/null +++ b/homeassistant/components/neato/.translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + }, + "create_entry": { + "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + }, + "error": { + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "unexpected_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d", + "vendor": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c" + }, + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "title": "Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/sl.json b/homeassistant/components/neato/.translations/sl.json new file mode 100644 index 00000000000000..7acbb718d17631 --- /dev/null +++ b/homeassistant/components/neato/.translations/sl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u017de konfigurirano", + "invalid_credentials": "Neveljavne poverilnice" + }, + "create_entry": { + "default": "Glejte [neato dokumentacija] ({docs_url})." + }, + "error": { + "invalid_credentials": "Neveljavne poverilnice", + "unexpected_error": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime", + "vendor": "Prodajalec" + }, + "description": "Glejte [neato dokumentacija] ({docs_url}).", + "title": "Podatki o ra\u010dunu Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/zh-Hant.json b/homeassistant/components/neato/.translations/zh-Hant.json new file mode 100644 index 00000000000000..61f49cd5da0010 --- /dev/null +++ b/homeassistant/components/neato/.translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "invalid_credentials": "\u6191\u8b49\u7121\u6548" + }, + "create_entry": { + "default": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002" + }, + "error": { + "invalid_credentials": "\u6191\u8b49\u7121\u6548", + "unexpected_error": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "vendor": "\u5ee0\u5546" + }, + "description": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002", + "title": "Neato \u5e33\u865f\u8cc7\u8a0a" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index c7e89c368c178c..7345cf462957c9 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c", "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e" }, diff --git a/homeassistant/components/opentherm_gw/.translations/ca.json b/homeassistant/components/opentherm_gw/.translations/ca.json new file mode 100644 index 00000000000000..0224d663a83953 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Passarel\u00b7la ja configurada", + "id_exists": "L'identificador de passarel\u00b7la ja existeix", + "serial_error": "S'ha produ\u00eft un error en connectar-se al dispositiu", + "timeout": "S'ha acabat el temps d'espera en l'intent de connexi\u00f3" + }, + "step": { + "init": { + "data": { + "device": "Ruta o URL", + "floor_temperature": "Temperatura del pis", + "id": "ID", + "name": "Nom", + "precision": "Precisi\u00f3 de la temperatura" + }, + "title": "Passarel\u00b7la d'OpenTherm" + } + }, + "title": "Passarel\u00b7la d'OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json new file mode 100644 index 00000000000000..b8abb48af4e89c --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/da.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "already_configured": "Gateway allerede konfigureret", + "id_exists": "Gateway-id findes allerede", + "serial_error": "Fejl ved tilslutning til enheden" + }, + "step": { + "init": { + "data": { + "device": "Sti eller URL", + "id": "ID", + "name": "Navn" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json new file mode 100644 index 00000000000000..274dd46488b8d6 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Gateway bereits konfiguriert", + "id_exists": "Gateway-ID ist bereits vorhanden", + "serial_error": "Fehler beim Verbinden mit dem Ger\u00e4t", + "timeout": "Zeit\u00fcberschreitung beim Verbindungsversuch" + }, + "step": { + "init": { + "data": { + "device": "Pfad oder URL", + "id": "ID", + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json new file mode 100644 index 00000000000000..65d7d9e92bb1bd --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway already configured", + "id_exists": "Gateway id already exists", + "serial_error": "Error connecting to device", + "timeout": "Connection attempt timed out" + }, + "step": { + "init": { + "data": { + "device": "Path or URL", + "floor_temperature": "Floor climate temperature", + "id": "ID", + "name": "Name", + "precision": "Climate temperature precision" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/es.json b/homeassistant/components/opentherm_gw/.translations/es.json new file mode 100644 index 00000000000000..8ad9d89b07a57d --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway ya configurado", + "id_exists": "El ID del Gateway ya existe", + "serial_error": "Error de conexi\u00f3n al dispositivo", + "timeout": "Intento de conexi\u00f3n agotado" + }, + "step": { + "init": { + "data": { + "device": "Ruta o URL", + "floor_temperature": "Temperatura del suelo", + "id": "ID", + "name": "Nombre", + "precision": "Precisi\u00f3n de la temperatura clim\u00e1tica" + }, + "title": "Gateway OpenTherm" + } + }, + "title": "Gateway OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json new file mode 100644 index 00000000000000..82b9a7aee88167 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "already_configured": "Passerelle d\u00e9j\u00e0 configur\u00e9e", + "id_exists": "L'identifiant de la passerelle existe d\u00e9j\u00e0", + "serial_error": "Erreur de connexion \u00e0 l'appareil", + "timeout": "La tentative de connexion a expir\u00e9" + }, + "step": { + "init": { + "data": { + "device": "Chemin ou URL", + "id": "ID", + "name": "Nom", + "precision": "Pr\u00e9cision de la temp\u00e9rature climatique" + }, + "title": "Passerelle OpenTherm" + } + }, + "title": "Passerelle OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/it.json b/homeassistant/components/opentherm_gw/.translations/it.json new file mode 100644 index 00000000000000..9c62686e19083d --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway gi\u00e0 configurato", + "id_exists": "ID del gateway esiste gi\u00e0", + "serial_error": "Errore durante la connessione al dispositivo", + "timeout": "Tentativo di connessione scaduto" + }, + "step": { + "init": { + "data": { + "device": "Percorso o URL", + "floor_temperature": "Temperatura climatica del pavimento", + "id": "ID", + "name": "Nome", + "precision": "Precisione della temperatura climatica" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "Gateway OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/lb.json b/homeassistant/components/opentherm_gw/.translations/lb.json new file mode 100644 index 00000000000000..ec1f719a6cc3d5 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway ass scho konfigur\u00e9iert", + "id_exists": "Gateway ID g\u00ebtt et schonn", + "serial_error": "Feeler beim verbannen", + "timeout": "Z\u00e4it Iwwerschreidung beim Verbindungs Versuch" + }, + "step": { + "init": { + "data": { + "device": "Pfad oder URL", + "floor_temperature": "Buedem Klima Temperatur", + "id": "ID", + "name": "Numm", + "precision": "Klima Temperatur Prezisioun" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json new file mode 100644 index 00000000000000..4fec1baba7badb --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "device": "Pad of URL", + "id": "ID" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nn.json b/homeassistant/components/opentherm_gw/.translations/nn.json new file mode 100644 index 00000000000000..3d018a2292d0c2 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "id": "ID", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json new file mode 100644 index 00000000000000..6104aa7de724cb --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway er allerede konfigurert", + "id_exists": "Gateway-ID finnes allerede", + "serial_error": "Feil ved tilkobling til enhet", + "timeout": "Tilkoblingsfors\u00f8k ble tidsavbrutt" + }, + "step": { + "init": { + "data": { + "device": "Bane eller URL-adresse", + "floor_temperature": "Gulv klimatemperatur", + "id": "ID", + "name": "Navn", + "precision": "Klima temperaturpresisjon" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json new file mode 100644 index 00000000000000..7e4a0eed013f3e --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "device": "\u015acie\u017cka lub adres URL", + "name": "Nazwa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/ru.json b/homeassistant/components/opentherm_gw/.translations/ru.json new file mode 100644 index 00000000000000..718322ec171088 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", + "serial_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "init": { + "data": { + "device": "\u041f\u0443\u0442\u044c \u0438\u043b\u0438 URL-\u0430\u0434\u0440\u0435\u0441", + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430", + "id": "ID", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b" + }, + "title": "OpenTherm" + } + }, + "title": "OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/sl.json b/homeassistant/components/opentherm_gw/.translations/sl.json new file mode 100644 index 00000000000000..5de551d5d0c0e9 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Prehod je \u017ee konfiguriran", + "id_exists": "ID prehoda \u017ee obstaja", + "serial_error": "Napaka pri povezovanju z napravo", + "timeout": "Poskus povezave je potekel" + }, + "step": { + "init": { + "data": { + "device": "Pot ali URL", + "floor_temperature": "Temperatura nadstropja", + "id": "ID", + "name": "Ime", + "precision": "Natan\u010dnost temperature " + }, + "title": "OpenTherm Prehod" + } + }, + "title": "OpenTherm Prehod" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json new file mode 100644 index 00000000000000..648f156e8643b1 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "\u9598\u9053\u5668\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728", + "serial_error": "\u9023\u7dda\u81f3\u88dd\u7f6e\u932f\u8aa4", + "timeout": "\u9023\u7dda\u5617\u8a66\u903e\u6642" + }, + "step": { + "init": { + "data": { + "device": "\u8def\u5f91\u6216 URL", + "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6", + "id": "ID", + "name": "\u540d\u7a31", + "precision": "\u6eab\u63a7\u7cbe\u6e96\u5ea6" + }, + "title": "OpenTherm \u9598\u9053\u5668" + } + }, + "title": "OpenTherm \u9598\u9053\u5668" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/ru.json b/homeassistant/components/openuv/.translations/ru.json index 9683c5d7c3679b..58d57b280567f1 100644 --- a/homeassistant/components/openuv/.translations/ru.json +++ b/homeassistant/components/openuv/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b", + "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b.", "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API" }, "step": { diff --git a/homeassistant/components/owntracks/.translations/nn.json b/homeassistant/components/owntracks/.translations/nn.json new file mode 100644 index 00000000000000..cdfd651beecffc --- /dev/null +++ b/homeassistant/components/owntracks/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/nn.json b/homeassistant/components/plaato/.translations/nn.json new file mode 100644 index 00000000000000..750e14b1daeeaa --- /dev/null +++ b/homeassistant/components/plaato/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Plaato Airlock" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 11e11ebc6fe85b..a3ba5185371eef 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -32,6 +32,10 @@ "description": "Hi ha diversos servidors disponibles, selecciona'n un:", "title": "Selecciona servidor Plex" }, + "start_website_auth": { + "description": "Continua l'autoritzaci\u00f3 a plex.tv.", + "title": "Connexi\u00f3 amb el servidor Plex" + }, "user": { "data": { "manual_setup": "Configuraci\u00f3 manual", diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json new file mode 100644 index 00000000000000..95083102273a1b --- /dev/null +++ b/homeassistant/components/plex/.translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden" + }, + "step": { + "manual_setup": { + "title": "Plex Server" + }, + "start_website_auth": { + "description": "Weiter zur Autorisierung unter plex.tv.", + "title": "Plex Server verbinden" + }, + "user": { + "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell." + } + } + }, + "options": { + "step": { + "plex_mp_settings": { + "description": "Optionen f\u00fcr Plex-Media-Player" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index efdd75b14819ab..bf927b7f1be07b 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -4,6 +4,7 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", + "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" @@ -32,6 +33,10 @@ "description": "Multiple servers available, select one:", "title": "Select Plex server" }, + "start_website_auth": { + "description": "Continue to authorize at plex.tv.", + "title": "Connect Plex server" + }, "user": { "data": { "manual_setup": "Manual setup", diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json index 6d1ad1f62daa8c..261ca9514905ec 100644 --- a/homeassistant/components/plex/.translations/es.json +++ b/homeassistant/components/plex/.translations/es.json @@ -4,7 +4,9 @@ "all_configured": "Todos los servidores vinculados ya configurados", "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "Plex se est\u00e1 configurando", + "discovery_no_file": "No se ha encontrado ning\u00fan archivo de configuraci\u00f3n antiguo", "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Fall\u00f3 por razones desconocidas" }, "error": { @@ -31,6 +33,10 @@ "description": "Varios servidores disponibles, seleccione uno:", "title": "Seleccione el servidor Plex" }, + "start_website_auth": { + "description": "Contin\u00fae en plex.tv para autorizar", + "title": "Conectar servidor Plex" + }, "user": { "data": { "manual_setup": "Configuraci\u00f3n manual", diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index 812de425ef49f2..c9e61dcf2e9407 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -5,18 +5,25 @@ "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "Plex en cours de configuration", "invalid_import": "La configuration import\u00e9e est invalide", + "token_request_timeout": "D\u00e9lai d'obtention du jeton", "unknown": "\u00c9chec pour une raison inconnue" }, "error": { "faulty_credentials": "L'autorisation \u00e0 \u00e9chou\u00e9e", "no_servers": "Aucun serveur li\u00e9 au compte", + "no_token": "Fournir un jeton ou s\u00e9lectionner l'installation manuelle", "not_found": "Serveur Plex introuvable" }, "step": { "manual_setup": { "data": { - "port": "Port" - } + "host": "H\u00f4te", + "port": "Port", + "ssl": "Utiliser SSL", + "token": "Jeton (si n\u00e9cessaire)", + "verify_ssl": "V\u00e9rifier le certificat SSL" + }, + "title": "Serveur Plex" }, "select_server": { "data": { @@ -27,6 +34,7 @@ }, "user": { "data": { + "manual_setup": "Installation manuelle", "token": "Jeton plex" }, "description": "Entrez un jeton Plex pour la configuration automatique.", @@ -34,5 +42,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Afficher tous les contr\u00f4les", + "use_episode_art": "Utiliser l'art de l'\u00e9pisode" + }, + "description": "Options pour lecteurs multim\u00e9dia Plex" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index 3c28f1d25f9c6b..8f61f968dba8c4 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -4,7 +4,9 @@ "all_configured": "Tutti i server collegati sono gi\u00e0 configurati", "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", "already_in_progress": "Plex \u00e8 in fase di configurazione", + "discovery_no_file": "Nessun file di configurazione legacy trovato", "invalid_import": "La configurazione importata non \u00e8 valida", + "token_request_timeout": "Timeout per l'ottenimento del token", "unknown": "Non riuscito per motivo sconosciuto" }, "error": { @@ -31,12 +33,16 @@ "description": "Sono disponibili pi\u00f9 server, selezionarne uno:", "title": "Selezionare il server Plex" }, + "start_website_auth": { + "description": "Continuare ad autorizzare su plex.tv.", + "title": "Collegare il server Plex" + }, "user": { "data": { "manual_setup": "Configurazione manuale", "token": "Token Plex" }, - "description": "Immettere un token Plex per la configurazione automatica.", + "description": "Continuare ad autorizzare plex.tv o configurare manualmente un server.", "title": "Collegare il server Plex" } }, diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 1e6488784d409a..7b0f7232976a67 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -4,7 +4,9 @@ "all_configured": "All verbonne Server sinn scho konfigur\u00e9iert", "already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert", "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", + "discovery_no_file": "Kee Konfiguratioun Fichier am ale Format fonnt.", "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", + "token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton", "unknown": "Onbekannte Feeler opgetrueden" }, "error": { @@ -31,6 +33,10 @@ "description": "M\u00e9i Server disponibel, wielt een aus:", "title": "Plex Server auswielen" }, + "start_website_auth": { + "description": "Weiderfueren op plex.tv fir d'Autorisatioun.", + "title": "Plex Server verbannen" + }, "user": { "data": { "manual_setup": "Manuell Konfiguratioun", diff --git a/homeassistant/components/plex/.translations/nn.json b/homeassistant/components/plex/.translations/nn.json new file mode 100644 index 00000000000000..a16deb2fca2899 --- /dev/null +++ b/homeassistant/components/plex/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index a0a9d087d1e58b..18c4e865a84690 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -4,6 +4,7 @@ "all_configured": "Alle knyttet servere som allerede er konfigurert", "already_configured": "Denne Plex-serveren er allerede konfigurert", "already_in_progress": "Plex blir konfigurert", + "discovery_no_file": "Ingen eldre konfigurasjonsfil ble funnet", "invalid_import": "Den importerte konfigurasjonen er ugyldig", "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Mislyktes av ukjent \u00e5rsak" @@ -32,6 +33,10 @@ "description": "Flere servere tilgjengelig, velg en:", "title": "Velg Plex-server" }, + "start_website_auth": { + "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv.", + "title": "Koble til Plex server" + }, "user": { "data": { "manual_setup": "Manuelt oppsett", diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index ce9d2e1e88d73c..9b75a0061e8d1b 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -5,6 +5,7 @@ "already_configured": "Serwer Plex jest ju\u017c skonfigurowany", "already_in_progress": "Plex jest konfigurowany", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", + "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena", "unknown": "Nieznany b\u0142\u0105d" }, "error": { diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index 2b63840d00198b..fe773f72be9721 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", - "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", - "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", + "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", + "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", + "discovery_no_file": "\u0421\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430", "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435" @@ -32,12 +33,16 @@ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u0438\u043d \u0438\u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432:", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex" }, + "start_website_auth": { + "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", + "title": "Plex" + }, "user": { "data": { "manual_setup": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "token": "\u0422\u043e\u043a\u0435\u043d" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", + "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", "title": "Plex" } }, diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 49ed34baf763b2..9be270a017ce4d 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -4,7 +4,9 @@ "all_configured": "Vsi povezani stre\u017eniki so \u017ee konfigurirani", "already_configured": "Ta stre\u017enik Plex je \u017ee konfiguriran", "already_in_progress": "Plex se konfigurira", + "discovery_no_file": "Podatkovne konfiguracijske datoteke ni bilo mogo\u010de najti", "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", + "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", "unknown": "Ni uspelo iz neznanega razloga" }, "error": { @@ -31,6 +33,10 @@ "description": "Na voljo je ve\u010d stre\u017enikov, izberite enega:", "title": "Izberite stre\u017enik Plex" }, + "start_website_auth": { + "description": "Nadaljujte z avtorizacijo na plex.tv.", + "title": "Pove\u017eite stre\u017enik Plex" + }, "user": { "data": { "manual_setup": "Ro\u010dna nastavitev", diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index 5f6d0c41c13506..2d4ce1ea6aa2c5 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -4,7 +4,9 @@ "all_configured": "\u6240\u6709\u7d81\u5b9a\u4f3a\u670d\u5668\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210", "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", + "discovery_no_file": "\u627e\u4e0d\u5230\u820a\u7248\u8a2d\u5b9a\u6a94\u6848", "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", + "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" }, "error": { @@ -31,12 +33,16 @@ "description": "\u627e\u5230\u591a\u500b\u4f3a\u670d\u5668\uff0c\u8acb\u9078\u64c7\u4e00\u7d44\uff1a", "title": "\u9078\u64c7 Plex \u4f3a\u670d\u5668" }, + "start_website_auth": { + "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u3002", + "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" + }, "user": { "data": { "manual_setup": "\u624b\u52d5\u8a2d\u5b9a", "token": "Plex \u5bc6\u9470" }, - "description": "\u8acb\u8f38\u5165 Plex \u5bc6\u9470\u4ee5\u9032\u884c\u81ea\u52d5\u6216\u624b\u52d5\u8a2d\u5b9a\u4f3a\u670d\u5668\u3002", + "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u6216\u624b\u52d5\u8a2d\u5b9a\u4f3a\u670d\u5668\u3002", "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" } }, diff --git a/homeassistant/components/point/.translations/nn.json b/homeassistant/components/point/.translations/nn.json new file mode 100644 index 00000000000000..865155c04949a7 --- /dev/null +++ b/homeassistant/components/point/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Minut Point" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/nn.json b/homeassistant/components/ps4/.translations/nn.json index b3302389c88c9a..86920906003ecc 100644 --- a/homeassistant/components/ps4/.translations/nn.json +++ b/homeassistant/components/ps4/.translations/nn.json @@ -5,9 +5,20 @@ "port_997_bind_error": "Kunne ikkje binde til port 997. Sj\u00e5 [dokumentasjonen] (https://www.home-assistant.io/components/ps4/) for ytterlegare informasjon." }, "step": { + "creds": { + "title": "Playstation 4" + }, + "link": { + "data": { + "code": "PIN", + "name": "Namn" + }, + "title": "Playstation 4" + }, "mode": { "title": "Playstation 4" } - } + }, + "title": "Playstation 4" } } \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/pl.json b/homeassistant/components/rainmachine/.translations/pl.json index 9ab6156549d5a1..cf842efe9f6d0f 100644 --- a/homeassistant/components/rainmachine/.translations/pl.json +++ b/homeassistant/components/rainmachine/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "Konto jest ju\u017c zarejestrowane", - "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { "user": { diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index 6eec3ef0ebac07..6248890389d140 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" }, "step": { diff --git a/homeassistant/components/sensor/.translations/ca.json b/homeassistant/components/sensor/.translations/ca.json new file mode 100644 index 00000000000000..59db5a62f86062 --- /dev/null +++ b/homeassistant/components/sensor/.translations/ca.json @@ -0,0 +1,24 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Nivell de bateria de {entity_name}", + "is_humidity": "Humitat de {entity_name}", + "is_illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "is_pressure": "Pressi\u00f3 de {entity_name}", + "is_signal_strength": "For\u00e7a del senyal de {entity_name}", + "is_temperature": "Temperatura de {entity_name}", + "is_timestamp": "Marca de temps de {entity_name}", + "is_value": "Valor de {entity_name}" + }, + "trigger_type": { + "battery_level": "Nivell de bateria de {entity_name}", + "humidity": "Humitat de {entity_name}", + "illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "pressure": "Pressi\u00f3 de {entity_name}", + "signal_strength": "For\u00e7a del senyal de {entity_name}", + "temperature": "Temperatura de {entity_name}", + "timestamp": "Marca de temps de {entity_name}", + "value": "Valor de {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/da.json b/homeassistant/components/sensor/.translations/da.json new file mode 100644 index 00000000000000..df9b9935dc149c --- /dev/null +++ b/homeassistant/components/sensor/.translations/da.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} batteriniveau", + "is_humidity": "{entity_name} fugtighed", + "is_illuminance": "{entity_name} belysningsstyrke", + "is_power": "{entity_name} str\u00f8m", + "is_pressure": "{entity_name} tryk", + "is_signal_strength": "{entity_name} signalstyrke", + "is_temperature": "{entity_name} temperatur", + "is_timestamp": "{entity_name} tidsstempel", + "is_value": "{entity_name} v\u00e6rdi" + }, + "trigger_type": { + "battery_level": "{entity_name} batteriniveau", + "humidity": "{entity_name} fugtighed", + "illuminance": "{entity_name} belysningsstyrke", + "power": "{entity_name} str\u00f8m", + "pressure": "{entity_name} tryk", + "signal_strength": "{entity_name} signalstyrke", + "temperature": "{entity_name} temperatur", + "timestamp": "{entity_name} tidsstempel", + "value": "{entity_name} v\u00e6rdi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/de.json b/homeassistant/components/sensor/.translations/de.json new file mode 100644 index 00000000000000..1f248099df34e5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/de.json @@ -0,0 +1,21 @@ +{ + "device_automation": { + "condition_type": { + "is_humidity": "{entity_name} Feuchtigkeit", + "is_pressure": "{entity_name} Druck", + "is_signal_strength": "{entity_name} Signalst\u00e4rke", + "is_temperature": "{entity_name} Temperatur", + "is_timestamp": "{entity_name} Zeitstempel", + "is_value": "{entity_name} Wert" + }, + "trigger_type": { + "battery_level": "{entity_name} Batteriestatus", + "humidity": "{entity_name} Feuchtigkeit", + "pressure": "{entity_name} Druck", + "signal_strength": "{entity_name} Signalst\u00e4rke", + "temperature": "{entity_name} Temperatur", + "timestamp": "{entity_name} Zeitstempel", + "value": "{entity_name} Wert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/en.json b/homeassistant/components/sensor/.translations/en.json new file mode 100644 index 00000000000000..7bbbe660feb94c --- /dev/null +++ b/homeassistant/components/sensor/.translations/en.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} battery level", + "is_humidity": "{entity_name} humidity", + "is_illuminance": "{entity_name} illuminance", + "is_power": "{entity_name} power", + "is_pressure": "{entity_name} pressure", + "is_signal_strength": "{entity_name} signal strength", + "is_temperature": "{entity_name} temperature", + "is_timestamp": "{entity_name} timestamp", + "is_value": "{entity_name} value" + }, + "trigger_type": { + "battery_level": "{entity_name} battery level", + "humidity": "{entity_name} humidity", + "illuminance": "{entity_name} illuminance", + "power": "{entity_name} power", + "pressure": "{entity_name} pressure", + "signal_strength": "{entity_name} signal strength", + "temperature": "{entity_name} temperature", + "timestamp": "{entity_name} timestamp", + "value": "{entity_name} value" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/es.json b/homeassistant/components/sensor/.translations/es.json new file mode 100644 index 00000000000000..a9039d2e410cf7 --- /dev/null +++ b/homeassistant/components/sensor/.translations/es.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} nivel de bater\u00eda", + "is_humidity": "{entity_name} humedad", + "is_illuminance": "{entity_name} iluminancia", + "is_power": "{entity_name} alimentaci\u00f3n", + "is_pressure": "{entity_name} presi\u00f3n", + "is_signal_strength": "{entity_name} intensidad de la se\u00f1al", + "is_temperature": "{entity_name} temperatura", + "is_timestamp": "{entity_name} marca de tiempo", + "is_value": "{entity_name} valor" + }, + "trigger_type": { + "battery_level": "{entity_name} nivel de bater\u00eda", + "humidity": "{entity_name} humedad", + "illuminance": "{entity_name} iluminancia", + "power": "{entity_name} alimentaci\u00f3n", + "pressure": "{entity_name} presi\u00f3n", + "signal_strength": "{entity_name} intensidad de la se\u00f1al", + "temperature": "{entity_name} temperatura", + "timestamp": "{entity_name} marca de tiempo", + "value": "{entity_name} valor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json new file mode 100644 index 00000000000000..676a5aa413fe9c --- /dev/null +++ b/homeassistant/components/sensor/.translations/fr.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} niveau batterie", + "is_humidity": "{entity_name} humidit\u00e9", + "is_illuminance": "{entity_name} \u00e9clairement", + "is_power": "{entity_name} puissance", + "is_pressure": "{entity_name} pression", + "is_signal_strength": "{entity_name} force du signal", + "is_temperature": "{entity_name} temp\u00e9rature", + "is_timestamp": "{entity_name} horodatage", + "is_value": "{entity_name} valeur" + }, + "trigger_type": { + "battery_level": "{entity_name} niveau batterie", + "humidity": "{entity_name} humidit\u00e9", + "illuminance": "{entity_name} \u00e9clairement", + "power": "{entity_name} puissance", + "pressure": "{entity_name} pression", + "signal_strength": "{entity_name} force du signal", + "temperature": "{entity_name} temp\u00e9rature", + "timestamp": "{entity_name} horodatage", + "value": "{entity_name} valeur" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/it.json b/homeassistant/components/sensor/.translations/it.json new file mode 100644 index 00000000000000..07b20245c1634d --- /dev/null +++ b/homeassistant/components/sensor/.translations/it.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Livello della batteria di {entity_name}", + "is_humidity": "Umidit\u00e0 di {entity_name}", + "is_illuminance": "Illuminazione di {entity_name}", + "is_power": "Potenza di {entity_name}", + "is_pressure": "Pressione di {entity_name}", + "is_signal_strength": "Potenza del segnale di {entity_name}", + "is_temperature": "Temperatura di {entity_name}", + "is_timestamp": "Data di {entity_name}", + "is_value": "Valore di {entity_name}" + }, + "trigger_type": { + "battery_level": "Livello della batteria di {entity_name}", + "humidity": "Umidit\u00e0 di {entity_name}", + "illuminance": "Illuminazione di {entity_name}", + "power": "Potenza di {entity_name}", + "pressure": "Pressione di {entity_name}", + "signal_strength": "Potenza del segnale di {entity_name}", + "temperature": "Temperatura di {entity_name}", + "timestamp": "Data di {entity_name}", + "value": "Valore di {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/lb.json b/homeassistant/components/sensor/.translations/lb.json new file mode 100644 index 00000000000000..01a4e89c9f4623 --- /dev/null +++ b/homeassistant/components/sensor/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} Batterie niveau", + "is_humidity": "{entity_name} Fiichtegkeet", + "is_illuminance": "{entity_name} Beliichtung", + "is_power": "{entity_name} Leeschtung", + "is_pressure": "{entity_name} Drock", + "is_signal_strength": "{entity_name} Signal St\u00e4erkt", + "is_temperature": "{entity_name} Temperatur", + "is_timestamp": "{entity_name} Z\u00e4itstempel", + "is_value": "{entity_name} W\u00e4ert" + }, + "trigger_type": { + "battery_level": "{entity_name} Batterie niveau", + "humidity": "{entity_name} Fiichtegkeet", + "illuminance": "{entity_name} Beliichtung", + "power": "{entity_name} Leeschtung", + "pressure": "{entity_name} Drock", + "signal_strength": "{entity_name} Signal St\u00e4erkt", + "temperature": "{entity_name} Temperatur", + "timestamp": "{entity_name} Z\u00e4itstempel", + "value": "{entity_name} W\u00e4ert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json new file mode 100644 index 00000000000000..5f5eeaacd11e75 --- /dev/null +++ b/homeassistant/components/sensor/.translations/no.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} batteriniv\u00e5", + "is_humidity": "{entity_name} fuktighet", + "is_illuminance": "{entity_name} belysningsstyrke", + "is_power": "{entity_name} str\u00f8m", + "is_pressure": "{entity_name} trykk", + "is_signal_strength": "{entity_name} signalstyrke", + "is_temperature": "{entity_name} temperatur", + "is_timestamp": "{entity_name} tidsstempel", + "is_value": "{entity_name} verdi" + }, + "trigger_type": { + "battery_level": "{entity_name} batteriniv\u00e5", + "humidity": "{entity_name} fuktighet", + "illuminance": "{entity_name} belysningsstyrke", + "power": "{entity_name} str\u00f8m", + "pressure": "{entity_name} trykk", + "signal_strength": "{entity_name} signalstyrke", + "temperature": "{entity_name} temperatur", + "timestamp": "{entity_name} tidsstempel", + "value": "{entity_name} verdi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/pl.json b/homeassistant/components/sensor/.translations/pl.json new file mode 100644 index 00000000000000..da1dcc1d6fd890 --- /dev/null +++ b/homeassistant/components/sensor/.translations/pl.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} poziom na\u0142adowania baterii", + "is_humidity": "{entity_name} wilgotno\u015b\u0107", + "is_temperature": "{entity_name} temperatura" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/sl.json b/homeassistant/components/sensor/.translations/sl.json new file mode 100644 index 00000000000000..e3bc994b6ea9b5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} raven baterije", + "is_humidity": "{entity_name} vla\u017enost", + "is_illuminance": "{entity_name} osvetlitev", + "is_power": "{entity_name} mo\u010d", + "is_pressure": "{entity_name} pritisk", + "is_signal_strength": "{entity_name} jakost signala", + "is_temperature": "{entity_name} temperatura", + "is_timestamp": "{entity_name} \u010dasovni \u017eig", + "is_value": "{entity_name} vrednost" + }, + "trigger_type": { + "battery_level": "{entity_name} raven baterije", + "humidity": "{entity_name} vla\u017enost", + "illuminance": "{entity_name} osvetljenosti", + "power": "{entity_name} mo\u010d", + "pressure": "{entity_name} tlak", + "signal_strength": "{entity_name} jakost signala", + "temperature": "{entity_name} temperatura", + "timestamp": "{entity_name} \u010dasovni \u017eig", + "value": "{entity_name} vrednost" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/zh-Hant.json b/homeassistant/components/sensor/.translations/zh-Hant.json new file mode 100644 index 00000000000000..af97681ee764d1 --- /dev/null +++ b/homeassistant/components/sensor/.translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} \u96fb\u91cf", + "is_humidity": "{entity_name} \u6fd5\u5ea6", + "is_illuminance": "{entity_name} \u7167\u5ea6", + "is_power": "{entity_name} \u96fb\u529b", + "is_pressure": "{entity_name} \u58d3\u529b", + "is_signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", + "is_temperature": "{entity_name} \u6eab\u5ea6", + "is_timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", + "is_value": "{entity_name} \u503c" + }, + "trigger_type": { + "battery_level": "{entity_name} \u96fb\u91cf", + "humidity": "{entity_name} \u6fd5\u5ea6", + "illuminance": "{entity_name} \u7167\u5ea6", + "power": "{entity_name} \u96fb\u529b", + "pressure": "{entity_name} \u58d3\u529b", + "signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", + "temperature": "{entity_name} \u6eab\u5ea6", + "timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", + "value": "{entity_name} \u503c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/nn.json b/homeassistant/components/simplisafe/.translations/nn.json new file mode 100644 index 00000000000000..0568cad3f6dc4b --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index f685297890eae7..e82172f92f8579 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" }, "step": { diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json index 88ea988ff1bb9a..03b17b3ba8b85c 100644 --- a/homeassistant/components/smhi/.translations/ru.json +++ b/homeassistant/components/smhi/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442", + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438" }, "step": { diff --git a/homeassistant/components/solaredge/.translations/ru.json b/homeassistant/components/solaredge/.translations/ru.json index fe36e4296feb92..d8622cdd2c1a3b 100644 --- a/homeassistant/components/solaredge/.translations/ru.json +++ b/homeassistant/components/solaredge/.translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "error": { - "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { "user": { diff --git a/homeassistant/components/soma/.translations/de.json b/homeassistant/components/soma/.translations/de.json new file mode 100644 index 00000000000000..d93eec8aed7442 --- /dev/null +++ b/homeassistant/components/soma/.translations/de.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Du kannst nur ein einziges Soma-Konto konfigurieren.", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json new file mode 100644 index 00000000000000..8126b6ea5aec63 --- /dev/null +++ b/homeassistant/components/soma/.translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "S\u00f3lo puede configurar una cuenta de Soma.", + "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", + "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/fr.json b/homeassistant/components/soma/.translations/fr.json new file mode 100644 index 00000000000000..e990fb98dc233a --- /dev/null +++ b/homeassistant/components/soma/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'un seul compte Soma.", + "authorize_url_timeout": "D\u00e9lai d'attente g\u00e9n\u00e9rant l'autorisation de l'URL.", + "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation." + }, + "create_entry": { + "default": "Authentifi\u00e9 avec succ\u00e8s avec Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/lb.json b/homeassistant/components/soma/.translations/lb.json new file mode 100644 index 00000000000000..d8aba082537bf7 --- /dev/null +++ b/homeassistant/components/soma/.translations/lb.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Soma Kont konfigur\u00e9ieren.", + "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", + "missing_configuration": "D'Soma Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich mat Soma authentifiz\u00e9iert." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/nn.json b/homeassistant/components/soma/.translations/nn.json new file mode 100644 index 00000000000000..6eeb4f75a3c5c4 --- /dev/null +++ b/homeassistant/components/soma/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json new file mode 100644 index 00000000000000..0ed881853b8afd --- /dev/null +++ b/homeassistant/components/soma/.translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Soma.", + "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono z Soma" + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/zh-Hant.json b/homeassistant/components/soma/.translations/zh-Hant.json new file mode 100644 index 00000000000000..3d28389ff9147c --- /dev/null +++ b/homeassistant/components/soma/.translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Soma \u5e33\u865f\u3002", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", + "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/nn.json b/homeassistant/components/somfy/.translations/nn.json new file mode 100644 index 00000000000000..ff0383c7f015cf --- /dev/null +++ b/homeassistant/components/somfy/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Somfy" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/nn.json b/homeassistant/components/tellduslive/.translations/nn.json new file mode 100644 index 00000000000000..934f56a420b353 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Telldus Live" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index afaaf4edbf5e5f..9d3c97ad902eb3 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" diff --git a/homeassistant/components/toon/.translations/nn.json b/homeassistant/components/toon/.translations/nn.json index b8dbeff27cacce..eed288a5e39a33 100644 --- a/homeassistant/components/toon/.translations/nn.json +++ b/homeassistant/components/toon/.translations/nn.json @@ -1,5 +1,12 @@ { "config": { + "step": { + "authenticate": { + "data": { + "username": "Brukarnamn" + } + } + }, "title": "Toon" } } \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/nn.json b/homeassistant/components/tplink/.translations/nn.json new file mode 100644 index 00000000000000..1d9fb41fc8ce11 --- /dev/null +++ b/homeassistant/components/tplink/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/nn.json b/homeassistant/components/traccar/.translations/nn.json new file mode 100644 index 00000000000000..9fc23b3e394074 --- /dev/null +++ b/homeassistant/components/traccar/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json new file mode 100644 index 00000000000000..ed0342b9430924 --- /dev/null +++ b/homeassistant/components/transmission/.translations/de.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "error": { + "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "wrong_credentials": "Falscher Benutzername oder Kennwort" + }, + "step": { + "options": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + }, + "title": "Konfigurationsoptionen" + }, + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index e1bc8dc322824a..67461d1a3e8565 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -20,7 +20,7 @@ "name": "Name", "password": "Password", "port": "Port", - "username": "User name" + "username": "Username" }, "title": "Setup Transmission Client" } diff --git a/homeassistant/components/transmission/.translations/fr.json b/homeassistant/components/transmission/.translations/fr.json new file mode 100644 index 00000000000000..e2360c016ca304 --- /dev/null +++ b/homeassistant/components/transmission/.translations/fr.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "error": { + "cannot_connect": "Impossible de se connecter \u00e0 l'h\u00f4te", + "wrong_credentials": "Mauvais nom d'utilisateur ou mot de passe" + }, + "step": { + "options": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + }, + "title": "Configurer les options" + }, + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "title": "Configuration du client Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + }, + "description": "Configurer les options pour Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/nn.json b/homeassistant/components/transmission/.translations/nn.json new file mode 100644 index 00000000000000..7622ac1b459096 --- /dev/null +++ b/homeassistant/components/transmission/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn", + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json index 5da2d4f9ef909b..e7a438cae11091 100644 --- a/homeassistant/components/transmission/.translations/ru.json +++ b/homeassistant/components/transmission/.translations/ru.json @@ -33,7 +33,7 @@ "data": { "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" }, - "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission" } } } diff --git a/homeassistant/components/twentemilieu/.translations/nn.json b/homeassistant/components/twentemilieu/.translations/nn.json new file mode 100644 index 00000000000000..02ac8ecf27a2e9 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/nn.json b/homeassistant/components/twilio/.translations/nn.json new file mode 100644 index 00000000000000..86e5d9051b339c --- /dev/null +++ b/homeassistant/components/twilio/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 3741b035d7a9d6..899b532290e929 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -38,6 +38,11 @@ "one": "un", "other": "altre" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Crea sensors d'\u00fas d'ample de banda per a clients de la xarxa" + } } } } diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json index 53b794ed4353ff..0d0315e49c752d 100644 --- a/homeassistant/components/unifi/.translations/da.json +++ b/homeassistant/components/unifi/.translations/da.json @@ -32,6 +32,11 @@ "track_devices": "Spor netv\u00e6rksenheder (Ubiquiti-enheder)", "track_wired_clients": "Inkluder kablede netv\u00e6rksklienter" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Opret b\u00e5ndbredde sensorer for netv\u00e6rksklienter" + } } } } diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index e447e89644f5b5..32a378b7c00c90 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -38,6 +38,11 @@ "one": "eins", "other": "andere" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Erstellen von Bandbreiten-Nutzungssensoren f\u00fcr Netzwerk-Clients" + } } } } diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 2025bad6246f1f..d9b65b6d1dab06 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -32,6 +32,11 @@ "track_devices": "Track network devices (Ubiquiti devices)", "track_wired_clients": "Include wired network clients" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients" + } } } } diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 0539f5607b4c36..1db6712142d5d8 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -38,6 +38,11 @@ "one": "uno", "other": "otro" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Crear sensores para monitorizar uso de ancho de banda de clientes de red" + } } } } diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index 8c2526f8a1565a..c40b7822073dc9 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -32,6 +32,11 @@ "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)", "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau" + } } } } diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json index 5285ed21873529..80b546ebcf8cd8 100644 --- a/homeassistant/components/unifi/.translations/it.json +++ b/homeassistant/components/unifi/.translations/it.json @@ -38,6 +38,11 @@ "one": "uno", "other": "altro" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Creare sensori di utilizzo della larghezza di banda per i client di rete" + } } } } diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json index 05b0ffc0c44cdf..4fa1f62c602b65 100644 --- a/homeassistant/components/unifi/.translations/lb.json +++ b/homeassistant/components/unifi/.translations/lb.json @@ -38,6 +38,11 @@ "one": "Een", "other": "M\u00e9i" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente erstellen" + } } } } diff --git a/homeassistant/components/unifi/.translations/nn.json b/homeassistant/components/unifi/.translations/nn.json new file mode 100644 index 00000000000000..7c129cba3afc71 --- /dev/null +++ b/homeassistant/components/unifi/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json index c21a47c7ea2c8b..9041f0184232ec 100644 --- a/homeassistant/components/unifi/.translations/no.json +++ b/homeassistant/components/unifi/.translations/no.json @@ -38,6 +38,11 @@ "one": "en", "other": "andre" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Opprett b\u00e5ndbreddesensorer for nettverksklienter" + } } } } diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json index 6366f82b3da0a8..5887460a8a5a56 100644 --- a/homeassistant/components/unifi/.translations/pl.json +++ b/homeassistant/components/unifi/.translations/pl.json @@ -40,6 +40,11 @@ "one": "Jeden", "other": "Inne" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Stw\u00f3rz sensory wykorzystania przepustowo\u015bci przez klient\u00f3w sieciowych" + } } } } diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index 76802a96367677..d7451bd81a0ce4 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c" }, "error": { @@ -32,6 +32,11 @@ "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)", "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u0421\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" + } } } } diff --git a/homeassistant/components/unifi/.translations/sl.json b/homeassistant/components/unifi/.translations/sl.json index 35000bf4e1f28d..7084c4609c5844 100644 --- a/homeassistant/components/unifi/.translations/sl.json +++ b/homeassistant/components/unifi/.translations/sl.json @@ -40,6 +40,11 @@ "other": "OSTALO", "two": "DVA" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Ustvarite senzorje porabe pasovne \u0161irine za omre\u017ene odjemalce" + } } } } diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json index 498afcbb10a19f..5e0b881af15200 100644 --- a/homeassistant/components/unifi/.translations/zh-Hant.json +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -32,6 +32,11 @@ "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09", "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u65b0\u589e\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668" + } } } } diff --git a/homeassistant/components/upnp/.translations/nn.json b/homeassistant/components/upnp/.translations/nn.json index 286efcf0353076..cfbedd994afa43 100644 --- a/homeassistant/components/upnp/.translations/nn.json +++ b/homeassistant/components/upnp/.translations/nn.json @@ -11,6 +11,7 @@ "init": { "title": "UPnP / IGD" } - } + }, + "title": "UPnP / IGD" } } \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 8d41ec1d5def31..3351f0d5d8a815 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP", "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP / IGD \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", diff --git a/homeassistant/components/vesync/.translations/nn.json b/homeassistant/components/vesync/.translations/nn.json new file mode 100644 index 00000000000000..372e37133b12c6 --- /dev/null +++ b/homeassistant/components/vesync/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/.translations/nn.json b/homeassistant/components/wemo/.translations/nn.json new file mode 100644 index 00000000000000..c1c8830cb25618 --- /dev/null +++ b/homeassistant/components/wemo/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Wemo" + } + }, + "title": "Wemo" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/nn.json b/homeassistant/components/withings/.translations/nn.json new file mode 100644 index 00000000000000..7d8b268367c374 --- /dev/null +++ b/homeassistant/components/withings/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/ru.json b/homeassistant/components/wwlln/.translations/ru.json index ad553def6c3ee7..3bdaf85498bcd4 100644 --- a/homeassistant/components/wwlln/.translations/ru.json +++ b/homeassistant/components/wwlln/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json index 0b800ecd80a7ef..39f254ac9af075 100644 --- a/homeassistant/components/zha/.translations/da.json +++ b/homeassistant/components/zha/.translations/da.json @@ -29,7 +29,10 @@ "button_3": "Tredje knap", "button_4": "Fjerde knap", "button_5": "Femte knap", + "button_6": "Sjette knap", "close": "Luk", + "dim_down": "D\u00e6mp ned", + "dim_up": "D\u00e6mp op", "left": "Venstre", "open": "\u00c5ben", "right": "H\u00f8jre" diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 280c941b427f4e..9ffd5211a1fca0 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -16,5 +16,13 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "close": "Schlie\u00dfen", + "left": "Links", + "open": "Offen", + "right": "Rechts" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index 48328aed878189..f8b78af5721ab4 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -16,5 +16,32 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Hurlement", + "warn": "Pr\u00e9venir" + }, + "trigger_subtype": { + "both_buttons": "Les deux boutons", + "button_1": "Premier bouton", + "button_2": "Deuxi\u00e8me bouton", + "button_3": "Troisi\u00e8me bouton", + "button_4": "Quatri\u00e8me bouton", + "button_5": "Cinqui\u00e8me bouton", + "button_6": "Sixi\u00e8me bouton", + "close": "Fermer", + "left": "Gauche", + "open": "Ouvert", + "right": "Droite", + "turn_off": "\u00c9teindre", + "turn_on": "Allumer" + }, + "trigger_type": { + "device_shaken": "Appareil secou\u00e9", + "device_tilted": "Dispositif inclin\u00e9", + "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", + "remote_button_triple_press": "Bouton\"{sous-type}\" \u00e0 trois clics" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nn.json b/homeassistant/components/zha/.translations/nn.json new file mode 100644 index 00000000000000..ad2c240baf18f2 --- /dev/null +++ b/homeassistant/components/zha/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 390069b7698c4e..18c4c3c9ff2a14 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -53,12 +53,12 @@ "device_rotated": "Enheten roterte \"{subtype}\"", "device_shaken": "Enhet er ristet", "device_slid": "Enheten skled \"{subtype}\"", - "device_tilted": "Enhetn skr\u00e5stilt", - "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", - "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", - "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\" {subtype} \" - knappen ble femdobbelt klikket", + "device_tilted": "Enheten skr\u00e5stilt", + "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen ble femdobbelt klikket", "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\"{subtype}\"-knappen ble trippel klikket" diff --git a/homeassistant/components/zone/.translations/ru.json b/homeassistant/components/zone/.translations/ru.json index dc408035d0f657..6a017e9e1c373e 100644 --- a/homeassistant/components/zone/.translations/ru.json +++ b/homeassistant/components/zone/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { "init": { diff --git a/homeassistant/components/zwave/.translations/nn.json b/homeassistant/components/zwave/.translations/nn.json index ebd9d44796c7d2..8d1c737170f35e 100644 --- a/homeassistant/components/zwave/.translations/nn.json +++ b/homeassistant/components/zwave/.translations/nn.json @@ -4,6 +4,7 @@ "user": { "description": "Sj\u00e5 [www.home-assistant.io/docs/z-wave/installation/](https://www.home-assistant.io/docs/z-wave/installation/) for informasjon om konfigurasjonsvariablene." } - } + }, + "title": "Z-Wave" } } \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json index a64b4db185d7a1..ed2e20f35270c2 100644 --- a/homeassistant/components/zwave/.translations/ru.json +++ b/homeassistant/components/zwave/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave" }, "error": { From d97943575ac5bcced97ad376bf70f07747aa27d6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Oct 2019 11:19:46 -0700 Subject: [PATCH 292/296] Bumped version to 0.100.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5bd5b7ea7650be..2ded4cde472f94 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From a57642833b78ee64763ef12787a80cf543b36ffb Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Tue, 8 Oct 2019 11:14:52 -0700 Subject: [PATCH 293/296] Fix connection issues with withings API by switching to a maintained codebase (#27310) * Fixing connection issues with withings API by switching to a maintained client codebase. * Updating requirements files. * Adding withings api to requirements script. --- homeassistant/components/withings/common.py | 14 +- .../components/withings/config_flow.py | 4 +- .../components/withings/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- script/gen_requirements_all.py | 2 +- tests/components/withings/common.py | 12 +- tests/components/withings/conftest.py | 139 +++++++++--------- tests/components/withings/test_common.py | 20 +-- tests/components/withings/test_sensor.py | 44 +++--- 10 files changed, 131 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index f2be849cbc794b..9acca6f0cd68e6 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -4,7 +4,7 @@ import re import time -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError from requests_oauthlib import TokenUpdated @@ -68,7 +68,9 @@ class WithingsDataManager: service_available = None - def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): + def __init__( + self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi + ): """Constructor.""" self._hass = hass self._api = api @@ -253,7 +255,7 @@ def create_withings_data_manager( """Set up the sensor config entry.""" entry_creds = entry.data.get(const.CREDENTIALS) or {} profile = entry.data[const.PROFILE] - credentials = nokia.NokiaCredentials( + credentials = withings.WithingsCredentials( entry_creds.get("access_token"), entry_creds.get("token_expiry"), entry_creds.get("token_type"), @@ -266,7 +268,7 @@ def create_withings_data_manager( def credentials_saver(credentials_param): _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - # Sanitizing the data as sometimes a NokiaCredentials object + # Sanitizing the data as sometimes a WithingsCredentials object # is passed through from the API. cred_data = credentials_param if not isinstance(credentials_param, dict): @@ -275,8 +277,8 @@ def credentials_saver(credentials_param): entry.data[const.CREDENTIALS] = cred_data hass.config_entries.async_update_entry(entry, data={**entry.data}) - _LOGGER.debug("Creating nokia api instance") - api = nokia.NokiaApi( + _LOGGER.debug("Creating withings api instance") + api = withings.WithingsApi( credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) ) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index f28a4f59d80195..c781e785f5e122 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -4,7 +4,7 @@ from typing import Optional import aiohttp -import nokia +import withings_api as withings import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -75,7 +75,7 @@ def get_auth_client(self, profile: str): profile, ) - return nokia.NokiaAuth( + return withings.WithingsAuth( client_id, client_secret, callback_uri, diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index d38b69f2248bcc..ae5cd4bcdd9ee9 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "nokia==1.2.0" + "withings-api==2.0.0b7" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 7aa5090688c092..c9869b25530af3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,9 +865,6 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.nederlandse_spoorwegen nsapi==2.7.4 @@ -1973,6 +1970,9 @@ websockets==6.0 # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 +# homeassistant.components.withings +withings-api==2.0.0b7 + # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69c6ded580688f..03a0c13dfce0b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,9 +228,6 @@ minio==4.0.9 # homeassistant.components.ssdp netdisco==2.6.0 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow @@ -448,6 +445,9 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==1.1.6 +# homeassistant.components.withings +withings-api==2.0.0b7 + # homeassistant.components.zeroconf zeroconf==0.23.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e35a83bd24d7ea..8a33baabc2954f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -105,7 +105,6 @@ "mficlient", "minio", "netdisco", - "nokia", "numpy", "oauth2client", "paho-mqtt", @@ -182,6 +181,7 @@ "vultr", "wakeonlan", "warrant", + "withings-api", "YesssSMS", "zeroconf", "zigpy-homeassistant", diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index b8406c39711e57..f3839a1be5524c 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,7 +1,7 @@ """Common data for for the withings component tests.""" import time -import nokia +import withings_api as withings import homeassistant.components.withings.const as const @@ -92,7 +92,7 @@ def new_measure(type_str, value, unit): } -def nokia_sleep_response(states): +def withings_sleep_response(states): """Create a sleep response based on states.""" data = [] for state in states: @@ -104,10 +104,10 @@ def nokia_sleep_response(states): ) ) - return nokia.NokiaSleep(new_sleep_data("aa", data)) + return withings.WithingsSleep(new_sleep_data("aa", data)) -NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( +WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( { "updatetime": "", "timezone": "", @@ -174,7 +174,7 @@ def nokia_sleep_response(states): ) -NOKIA_SLEEP_RESPONSE = nokia_sleep_response( +WITHINGS_SLEEP_RESPONSE = withings_sleep_response( [ const.MEASURE_TYPE_SLEEP_STATE_AWAKE, const.MEASURE_TYPE_SLEEP_STATE_LIGHT, @@ -183,7 +183,7 @@ def nokia_sleep_response(states): ] ) -NOKIA_SLEEP_SUMMARY_RESPONSE = nokia.NokiaSleepSummary( +WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( { "series": [ new_sleep_summary( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 7cbe3dc1cd4cd0..0aa6af0d7c098a 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ from typing import Awaitable, Callable, List import asynctest -import nokia +import withings_api as withings import pytest import homeassistant.components.api as api @@ -15,9 +15,9 @@ from homeassistant.setup import async_setup_component from .common import ( - NOKIA_MEASURES_RESPONSE, - NOKIA_SLEEP_RESPONSE, - NOKIA_SLEEP_SUMMARY_RESPONSE, + WITHINGS_MEASURES_RESPONSE, + WITHINGS_SLEEP_RESPONSE, + WITHINGS_SLEEP_SUMMARY_RESPONSE, ) @@ -34,17 +34,17 @@ def __init__( measures: List[str] = None, unit_system: str = None, throttle_interval: int = const.THROTTLE_INTERVAL, - nokia_request_response="DATA", - nokia_measures_response: nokia.NokiaMeasures = NOKIA_MEASURES_RESPONSE, - nokia_sleep_response: nokia.NokiaSleep = NOKIA_SLEEP_RESPONSE, - nokia_sleep_summary_response: nokia.NokiaSleepSummary = NOKIA_SLEEP_SUMMARY_RESPONSE, + withings_request_response="DATA", + withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, + withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, + withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, ) -> None: """Constructor.""" self._throttle_interval = throttle_interval - self._nokia_request_response = nokia_request_response - self._nokia_measures_response = nokia_measures_response - self._nokia_sleep_response = nokia_sleep_response - self._nokia_sleep_summary_response = nokia_sleep_summary_response + self._withings_request_response = withings_request_response + self._withings_measures_response = withings_measures_response + self._withings_sleep_response = withings_sleep_response + self._withings_sleep_summary_response = withings_sleep_summary_response self._withings_config = { const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret", @@ -103,24 +103,24 @@ def throttle_interval(self): return self._throttle_interval @property - def nokia_request_response(self): + def withings_request_response(self): """Request response.""" - return self._nokia_request_response + return self._withings_request_response @property - def nokia_measures_response(self) -> nokia.NokiaMeasures: + def withings_measures_response(self) -> withings.WithingsMeasures: """Measures response.""" - return self._nokia_measures_response + return self._withings_measures_response @property - def nokia_sleep_response(self) -> nokia.NokiaSleep: + def withings_sleep_response(self) -> withings.WithingsSleep: """Sleep response.""" - return self._nokia_sleep_response + return self._withings_sleep_response @property - def nokia_sleep_summary_response(self) -> nokia.NokiaSleepSummary: + def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: """Sleep summary response.""" - return self._nokia_sleep_summary_response + return self._withings_sleep_summary_response class WithingsFactoryData: @@ -130,21 +130,21 @@ def __init__( self, hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ): """Constructor.""" self._hass = hass self._flow_id = flow_id - self._nokia_auth_get_credentials_mock = nokia_auth_get_credentials_mock - self._nokia_api_request_mock = nokia_api_request_mock - self._nokia_api_get_measures_mock = nokia_api_get_measures_mock - self._nokia_api_get_sleep_mock = nokia_api_get_sleep_mock - self._nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_mock + self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock + self._withings_api_request_mock = withings_api_request_mock + self._withings_api_get_measures_mock = withings_api_get_measures_mock + self._withings_api_get_sleep_mock = withings_api_get_sleep_mock + self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock self._data_manager_get_throttle_interval_mock = ( data_manager_get_throttle_interval_mock ) @@ -160,29 +160,29 @@ def flow_id(self): return self._flow_id @property - def nokia_auth_get_credentials_mock(self): + def withings_auth_get_credentials_mock(self): """Get auth credentials mock.""" - return self._nokia_auth_get_credentials_mock + return self._withings_auth_get_credentials_mock @property - def nokia_api_request_mock(self): + def withings_api_request_mock(self): """Get request mock.""" - return self._nokia_api_request_mock + return self._withings_api_request_mock @property - def nokia_api_get_measures_mock(self): + def withings_api_get_measures_mock(self): """Get measures mock.""" - return self._nokia_api_get_measures_mock + return self._withings_api_get_measures_mock @property - def nokia_api_get_sleep_mock(self): + def withings_api_get_sleep_mock(self): """Get sleep mock.""" - return self._nokia_api_get_sleep_mock + return self._withings_api_get_sleep_mock @property - def nokia_api_get_sleep_summary_mock(self): + def withings_api_get_sleep_summary_mock(self): """Get sleep summary mock.""" - return self._nokia_api_get_sleep_summary_mock + return self._withings_api_get_sleep_summary_mock @property def data_manager_get_throttle_interval_mock(self): @@ -243,9 +243,9 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: assert await async_setup_component(hass, http.DOMAIN, config.hass_config) assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - nokia_auth_get_credentials_patch = asynctest.patch( - "nokia.NokiaAuth.get_credentials", - return_value=nokia.NokiaCredentials( + withings_auth_get_credentials_patch = asynctest.patch( + "withings_api.WithingsAuth.get_credentials", + return_value=withings.WithingsCredentials( access_token="my_access_token", token_expiry=time.time() + 600, token_type="my_token_type", @@ -255,28 +255,33 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: consumer_secret=config.withings_config.get(const.CLIENT_SECRET), ), ) - nokia_auth_get_credentials_mock = nokia_auth_get_credentials_patch.start() + withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() - nokia_api_request_patch = asynctest.patch( - "nokia.NokiaApi.request", return_value=config.nokia_request_response + withings_api_request_patch = asynctest.patch( + "withings_api.WithingsApi.request", + return_value=config.withings_request_response, ) - nokia_api_request_mock = nokia_api_request_patch.start() + withings_api_request_mock = withings_api_request_patch.start() - nokia_api_get_measures_patch = asynctest.patch( - "nokia.NokiaApi.get_measures", return_value=config.nokia_measures_response + withings_api_get_measures_patch = asynctest.patch( + "withings_api.WithingsApi.get_measures", + return_value=config.withings_measures_response, ) - nokia_api_get_measures_mock = nokia_api_get_measures_patch.start() + withings_api_get_measures_mock = withings_api_get_measures_patch.start() - nokia_api_get_sleep_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep", return_value=config.nokia_sleep_response + withings_api_get_sleep_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep", + return_value=config.withings_sleep_response, ) - nokia_api_get_sleep_mock = nokia_api_get_sleep_patch.start() + withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() - nokia_api_get_sleep_summary_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep_summary", - return_value=config.nokia_sleep_summary_response, + withings_api_get_sleep_summary_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep_summary", + return_value=config.withings_sleep_summary_response, + ) + withings_api_get_sleep_summary_mock = ( + withings_api_get_sleep_summary_patch.start() ) - nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_patch.start() data_manager_get_throttle_interval_patch = asynctest.patch( "homeassistant.components.withings.common.WithingsDataManager" @@ -295,11 +300,11 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: patches.extend( [ - nokia_auth_get_credentials_patch, - nokia_api_request_patch, - nokia_api_get_measures_patch, - nokia_api_get_sleep_patch, - nokia_api_get_sleep_summary_patch, + withings_auth_get_credentials_patch, + withings_api_request_patch, + withings_api_get_measures_patch, + withings_api_get_sleep_patch, + withings_api_get_sleep_summary_patch, data_manager_get_throttle_interval_patch, get_measures_patch, ] @@ -328,11 +333,11 @@ def create_task(*args): return WithingsFactoryData( hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index a22689f92bb6b5..9f2480f9094e55 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,6 +1,6 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError import pytest from requests_oauthlib import TokenUpdated @@ -13,19 +13,19 @@ from homeassistant.exceptions import PlatformNotReady -@pytest.fixture(name="nokia_api") -def nokia_api_fixture(): - """Provide nokia api.""" - nokia_api = nokia.NokiaApi.__new__(nokia.NokiaApi) - nokia_api.get_measures = MagicMock() - nokia_api.get_sleep = MagicMock() - return nokia_api +@pytest.fixture(name="withings_api") +def withings_api_fixture(): + """Provide withings api.""" + withings_api = withings.WithingsApi.__new__(withings.WithingsApi) + withings_api.get_measures = MagicMock() + withings_api.get_sleep = MagicMock() + return withings_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, nokia_api: nokia.NokiaApi): +def data_manager_fixture(hass, withings_api: withings.WithingsApi): """Provide data manager.""" - return WithingsDataManager(hass, "My Profile", nokia_api) + return WithingsDataManager(hass, "My Profile", withings_api) def test_print_service(): diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index da77910097be89..697d0a8b86413f 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,7 +2,12 @@ from unittest.mock import MagicMock, patch import asynctest -from nokia import NokiaApi, NokiaMeasures, NokiaSleep, NokiaSleepSummary +from withings_api import ( + WithingsApi, + WithingsMeasures, + WithingsSleep, + WithingsSleepSummary, +) import pytest from homeassistant.components.withings import DOMAIN @@ -15,7 +20,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from .common import nokia_sleep_response +from .common import withings_sleep_response from .conftest import WithingsFactory, WithingsFactoryConfig @@ -120,9 +125,9 @@ async def test_health_sensor_state_none( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=None, - nokia_sleep_response=None, - nokia_sleep_summary_response=None, + withings_measures_response=None, + withings_sleep_response=None, + withings_sleep_summary_response=None, ) ) @@ -153,9 +158,9 @@ async def test_health_sensor_state_empty( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=NokiaMeasures({"measuregrps": []}), - nokia_sleep_response=NokiaSleep({"series": []}), - nokia_sleep_summary_response=NokiaSleepSummary({"series": []}), + withings_measures_response=WithingsMeasures({"measuregrps": []}), + withings_sleep_response=WithingsSleep({"series": []}), + withings_sleep_summary_response=WithingsSleepSummary({"series": []}), ) ) @@ -201,7 +206,8 @@ async def test_sleep_state_throttled( data = await withings_factory( WithingsFactoryConfig( - measures=[measure], nokia_sleep_response=nokia_sleep_response(sleep_states) + measures=[measure], + withings_sleep_response=withings_sleep_response(sleep_states), ) ) @@ -257,16 +263,16 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): "expires_in": "2", } - original_nokia_api = NokiaApi - nokia_api_instance = None + original_withings_api = WithingsApi + withings_api_instance = None - def new_nokia_api(*args, **kwargs): - nonlocal nokia_api_instance - nokia_api_instance = original_nokia_api(*args, **kwargs) - nokia_api_instance.request = MagicMock() - return nokia_api_instance + def new_withings_api(*args, **kwargs): + nonlocal withings_api_instance + withings_api_instance = original_withings_api(*args, **kwargs) + withings_api_instance.request = MagicMock() + return withings_api_instance - nokia_api_patch = patch("nokia.NokiaApi", side_effect=new_nokia_api) + withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) session_patch = patch("requests_oauthlib.OAuth2Session") client_patch = patch("oauthlib.oauth2.WebApplicationClient") update_entry_patch = patch.object( @@ -275,7 +281,7 @@ def new_nokia_api(*args, **kwargs): wraps=hass.config_entries.async_update_entry, ) - with session_patch, client_patch, nokia_api_patch, update_entry_patch: + with session_patch, client_patch, withings_api_patch, update_entry_patch: async_add_entities = MagicMock() hass.config_entries.async_update_entry = MagicMock() config_entry = ConfigEntry( @@ -298,7 +304,7 @@ def new_nokia_api(*args, **kwargs): await async_setup_entry(hass, config_entry, async_add_entities) - nokia_api_instance.set_token(expected_creds) + withings_api_instance.set_token(expected_creds) new_creds = config_entry.data[const.CREDENTIALS] assert new_creds["access_token"] == "my_access_token2" From f9c4bb04e3b16343f843a21ff58fc6607354303a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Wed, 9 Oct 2019 14:47:43 +0200 Subject: [PATCH 294/296] Update zigpy-zigate to 0.4.1 (#27345) * Update zigpy-zigate to 0.4.1 Fix #27297 * Update zigpy-zigate to 0.4.1 Fix #27297 --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 59d9508ac338d8..9790fbffd06312 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.5.0", "zigpy-homeassistant==0.9.0", "zigpy-xbee-homeassistant==0.5.0", - "zigpy-zigate==0.4.0" + "zigpy-zigate==0.4.1" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index c9869b25530af3..7a8ce0780b7ae2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2041,7 +2041,7 @@ zigpy-homeassistant==0.9.0 zigpy-xbee-homeassistant==0.5.0 # homeassistant.components.zha -zigpy-zigate==0.4.0 +zigpy-zigate==0.4.1 # homeassistant.components.zoneminder zm-py==0.3.3 From 4dc50107cd4cddb295c5cce1c0758df154b5d879 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 9 Oct 2019 23:06:27 +0200 Subject: [PATCH 295/296] Updated frontend to 20191002.2 (#27370) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 58e5558781a2ed..67a66bc96125c2 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191002.1" + "home-assistant-frontend==20191002.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 04a68bf9633d4f..19acae64b16971 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191002.1 +home-assistant-frontend==20191002.2 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7a8ce0780b7ae2..44c10f8ff296ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -640,7 +640,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.1 +home-assistant-frontend==20191002.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03a0c13dfce0b4..2a8f51cd1956cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -182,7 +182,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.1 +home-assistant-frontend==20191002.2 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 6e86b8c42f1f46c45b74372335b0328734d0bd6c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Oct 2019 15:24:20 -0700 Subject: [PATCH 296/296] Bumped version to 0.100.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2ded4cde472f94..c5b566e5084fba 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0)