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 ad001e5604..0c6ac82894 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 0000000000..b6edb9be87 --- /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 0000000000..ba44d86c2e --- /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 0000000000..3e1ce22a23 --- /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 da36928d6b..8a4bb391da 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 4fb1c67a56..84a6a6e293 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 8a4bb391da..ee42e82f97 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 07adcfc79f..7125d3e9ed 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 3095d14061..6a2543e8b2 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 ee42e82f97..48fe33d9e5 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 1b59592410..6b74c09107 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 9b98914314..0f1277e0b0 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 90b85aaeba..f14e7b4c66 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 75dcac93dd..f024386aa0 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 0000000000..cf449ebb35 --- /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 00d03b12d0..6ab8727440 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 269715b7cc..8f5fec9b30 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 0000000000..698c28d99b --- /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 0000000000..eeffc9262e --- /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 0000000000..0607f4ab08 --- /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 0000000000..f930df1286 --- /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 419d4b7286..4e253741b0 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 48fe33d9e5..09b8ffb746 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 63ce5b707c..7c88e615fa 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 c9cc9b2d33..c13de45598 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -9,7 +9,7 @@ from . import DOMAIN as BMW_DOMAIN _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 @@ class BMWConnectedDriveSensor(BinarySensorDevice): 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 @@ class BMWConnectedDriveSensor(BinarySensorDevice): 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 011908d545..96d541b195 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -17,10 +17,10 @@ _LOGGER = logging.getLogger(__name__) 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_METRIC = { 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 eec3f67898..8c534a88d3 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 6a2543e8b2..e78661a04e 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 09b8ffb746..2075dab9a3 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 | 150 ++++++++++-------- 1 file changed, 88 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index e760f91070..8f01036da7 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.const import ( + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, + DEFAULT_TRACK_NEW, + DOMAIN, + SCAN_INTERVAL, + SOURCE_TYPE_BLUETOOTH, +) 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, - DEFAULT_TRACK_NEW, - SOURCE_TYPE_BLUETOOTH, - DOMAIN, -) -import homeassistant.util.dt as dt_util +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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -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 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 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(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 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 = [] +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() - # Load all known devices. - # We just need the devices so set consider_home and home range - # to 0 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 setup_scanner(hass, config, see, discovery_info=None): """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 0b7228fb56..a30d08f31f 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 0000000000..b0ad4660d0 --- /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 0000000000..a3536902c7 --- /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 263730ba58..d36de4acc1 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 0f1277e0b0..cc6d22945d 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 f14e7b4c66..7a2b883286 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 0ddff8557e..923a2beb2f 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 506461ea50..994e13f567 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 23e98919bb..92fd1e3e74 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 0000000000..74ae554175 --- /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 cf449ebb35..97971b99e9 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 cb4682fc93..947425e480 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 0000000000..201e3ff49c --- /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 0000000000..0948a31739 --- /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 0000000000..0321a6b73c --- /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 9e567fcc39..8c2526f8a1 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 f930df1286..8d93adbf4a 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 8943643e8b..3e2fea2342 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 | 126 +++++++++++------- 1 file changed, 75 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 8f01036da7..6a26775b0a 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 @@ from homeassistant.components.device_tracker.legacy import ( 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 + devices = await async_load_config(yaml_path, hass, 0) + bluetooth_devices = [device for device in devices if is_bluetooth_device(device)] - normalized_mac: str = device.mac[3:] - if device.track: - devices_to_track.add(normalized_mac) - else: - devices_to_not_track.add(normalized_mac) + 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) - rssi = None - if request_rssi: - client = BluetoothRSSI(mac) - rssi = client.request_rssi() - client.close() + device_name = await hass.async_add_executor_job(lookup_name, mac) if device_name is None: # Could not lookup device name continue - see_device(see, mac, device_name, rssi) + + rssi = None + if request_rssi: + client = BluetoothRSSI(mac) + rssi = await hass.async_add_executor_job(client.request_rssi) + client.close() + + 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.services.register(DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth) + hass.async_create_task(update_bluetooth()) + async_track_time_interval(hass, update_bluetooth, interval) + + 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 8c534a88d3..0c614a9dab 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 0c614a9dab..42815d8c8a 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 35cf63fc3d..a090dca0d0 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -3,7 +3,6 @@ import asyncio 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 @@ from .const import ( 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 @@ class DeconzGateway: ) ) - 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 @@ class DeconzGateway: 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 d84a47c6aa..a6138087f1 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -13,6 +13,7 @@ from homeassistant.util import slugify 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 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 0000000000..72966ba6c6 --- /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 c17aa0b663..d84706430f 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 971d38f9ab..46f21d9619 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 b6aa1cefb0..873dfee9a9 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 9135ed3b47..73749382dd 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 8bcf03914c..3d2b3f1781 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 994e13f567..70c33cf3c0 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 86210b2e6c..9aebb2a556 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 0000000000..7326d80497 --- /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 0000000000..e2a7f94b3d --- /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 8fc70a60a0..28999de5e8 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 b56875453d..93dfc65bbe 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 68e770e887..afd59d619e 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 0000000000..7c0d17c8a8 --- /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 0000000000..9f52511a16 --- /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 0000000000..ebfefe40b0 --- /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 0000000000..6749eab129 --- /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 0000000000..38edfe5a19 --- /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 ab8c0e70cd..b0b65a10c8 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 02dcb71f54..902e28b208 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 e60ef7b4c6..1acaaa53ab 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 379f69febb..27257e5039 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -27,9 +27,15 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class Light(ZhaEntity, light.Light): 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 @@ class Light(ZhaEntity, light.Light): 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 @@ class Light(ZhaEntity, light.Light): """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 @@ class Light(ZhaEntity, light.Light): 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 @@ class Light(ZhaEntity, light.Light): 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 @@ class Light(ZhaEntity, light.Light): 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 0c6ac82894..fef77c8a24 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 56a39df64c..dec91186be 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -7,7 +7,9 @@ from aiohttp import CookieJar import voluptuous as vol from iaqualink import ( + AqualinkBinarySensor, AqualinkClient, + AqualinkDevice, AqualinkLight, AqualinkLoginException, AqualinkSensor, @@ -16,6 +18,7 @@ from iaqualink import ( ) 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 @@ class AqualinkEntity(Entity): 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 0000000000..09c9322a58 --- /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 321c54329a..f41d17837c 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 fbfb10783e..813af7863f 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 4a1691e031..81021d0b44 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 f2fc51ce71..8efb473cf5 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 594f4f6c54..4ac4614379 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -38,7 +38,7 @@ from homeassistant.config_entries import ConfigEntry 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 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD @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 5eeb14b635..05853d4b26 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -12,6 +12,7 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class HomematicipGenericDevice(Entity): 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 43812df94d..770921288b 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -35,7 +35,7 @@ from homeassistant.const import ( 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 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): @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 058e21262e..ababf793f0 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -19,7 +19,7 @@ from homeassistant.config_entries import ConfigEntry 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 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): @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 56663c6b2d..af9f619cb3 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -10,10 +10,10 @@ from homeassistant.const import ( ) 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 @@ CONFIG_SCHEMA = vol.Schema( 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 0000000000..31ba0ff358 --- /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 2d0a69d7d0..ed9098559a 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -7,6 +7,7 @@ from homematicip.aio.device import ( 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 @@ from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice _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 492b16a603..b81ecdc516 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 @@ ATTR_VIBRATIONSTRENGTH = "vibrationstrength" 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 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): 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 1844cb2c97..b7a1ebce22 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -8,7 +8,7 @@ from homeassistant.components.climate.const import ( 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 @@ SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] 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 @@ class DeconzThermostat(DeconzDevice, ClimateDevice): """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 e6249b2304..68daee6cf2 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -24,10 +24,10 @@ class DeconzBase: @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 f6c2d471bb..31588db1f2 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -27,6 +27,11 @@ class DeconzEvent(DeconzBase): 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 a6138087f1..001721d4f0 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 @@ ATTR_EVENT_ID = "event_id" 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 @@ class DeconzSensor(DeconzDevice): 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 @@ class DeconzSensor(DeconzDevice): 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 @@ class DeconzBattery(DeconzDevice): 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 @@ class DeconzBattery(DeconzDevice): @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 @@ class DeconzBattery(DeconzDevice): @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 af9f619cb3..558b0fe420 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 12e2e092f6..c63b172139 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -191,31 +191,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # 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 3f00c31c7e..d7071d6dae 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 b0456e0b62..d058656552 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -6,7 +6,6 @@ import pytest 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 | 153 +++++------------- requirements_all.txt | 3 + 6 files changed, 149 insertions(+), 118 deletions(-) diff --git a/.coveragerc b/.coveragerc index fef77c8a24..824fb3828f 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 18218bbf68..fe5e19f911 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 2480daf2ea..563fe26109 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 69293ede51..17b11d6aef 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 73643a5383..ce1fda0839 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 @@ SENSOR_TYPES = { "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) + """Create NZBGet sensors.""" - url = f"http{ssl}://{host}:{port}/jsonrpc" + if discovery_info is None: + return - 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 + 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 @@ class NZBGetSensor(Entity): """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 @@ class NZBGetSensor(Entity): 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 2075dab9a3..e5ecd69ee2 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 847635d4da..fda5c74322 100644 --- a/tests/common.py +++ b/tests/common.py @@ -602,40 +602,40 @@ class MockEntityPlatform(entity_platform.EntityPlatform): ) -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 08a49c4a66..fb35485f5c 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 @@ async def test_flux_when_switch_is_off(hass): 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 @@ async def test_flux_before_sunrise(hass): 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 @@ async def test_flux_after_sunrise_before_sunset(hass): 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 @@ async def test_flux_after_sunset_before_stop(hass): 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 @@ async def test_flux_after_stop_before_sunrise(hass): 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 @@ async def test_flux_with_custom_start_stop_times(hass): 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 @@ async def test_flux_before_sunrise_stop_next_day(hass): 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 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day(hass): 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 @@ async def test_flux_after_sunset_before_midnight_stop_next_day(hass, x): 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 @@ async def test_flux_after_sunset_after_midnight_stop_next_day(hass): 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 @@ async def test_flux_after_stop_before_sunrise_stop_next_day(hass): 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 @@ async def test_flux_with_custom_colortemps(hass): 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 @@ async def test_flux_with_custom_brightness(hass): 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 @@ async def test_flux_with_multiple_lights(hass): 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 @@ async def test_flux_with_mired(hass): 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 @@ async def test_flux_with_rgb(hass): 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 367ea52b3a..776d8f39f6 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 40fa08856c..3525f1121c 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 dc4cb7502c..8ceda6cbd3 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -137,39 +137,39 @@ class TestLight(unittest.TestCase): 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 @@ class TestLight(unittest.TestCase): 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 @@ class TestLight(unittest.TestCase): # 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 @@ class TestLight(unittest.TestCase): 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 @@ class TestLight(unittest.TestCase): 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 @@ class TestLight(unittest.TestCase): 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 7047e6e8d9..5c8d46cb72 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -24,7 +24,7 @@ class TestScene(unittest.TestCase): 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 7dba734765..3bd29a72a1 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 c04a30589e..a9463cb78f 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -21,7 +21,7 @@ class TestSwitch(unittest.TestCase): 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 43338c9e14..0a48388b71 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -4,23 +4,23 @@ Provide a mock light platform. 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 f4226ecc63..484c47d119 100644 --- a/tests/testing_config/custom_components/test/switch.py +++ b/tests/testing_config/custom_components/test/switch.py @@ -4,23 +4,23 @@ Provide a mock switch platform. 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 0625fd4c27..2ae2006512 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 2413edaebc..244fda6165 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 e5ecd69ee2..056b51c345 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 5fa7d49b88..2afa18af32 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -308,7 +308,7 @@ class ColorSettingTrait(_Trait): 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 87d8134ccb..0b1291d404 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 @@ from homeassistant.core import State, callback 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 @@ class LightGroup(light.Light): 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 @@ class LightGroup(light.Light): 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 @@ class LightGroup(light.Light): 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 d3b0d8dd30..87898e42d5 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 563fe26109..37744dce18 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -3,7 +3,6 @@ from datetime import timedelta import logging import pynzbgetapi -import requests import voluptuous as vol from homeassistant.const import ( @@ -101,6 +100,6 @@ class NZBGetData: 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 31ba0ff358..3498b46d87 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 d058656552..7d630498cd 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -3,7 +3,6 @@ from unittest.mock import Mock, patch 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 0000000000..63934871fc --- /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 558c0c39f6..13f0915bc5 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 27257e5039..c2273c5407 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -201,7 +201,7 @@ class Light(ZhaEntity, light.Light): 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 9997ae00f0..d7282317d9 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 056b51c345..049e3a423c 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 46f21d9619..5886d8e5c5 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 0000000000..b0ad4660d0 --- /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 0000000000..b0ad4660d0 --- /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 817737eee0..d29481a3be 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 2cb0bd9af1..b10518646a 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 c088e414c7..3774956330 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 a02a6ff422..f3eead4aae 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 3d2b3f1781..1bc6c8211a 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 60a27304d7..41c75ec4aa 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 f9f2d40488..116f6254b3 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 8798248224..7a93c6ff9c 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 88730a1855..70d766cf4c 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 955b050bc5..882b67823b 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 41404822dd..f6f592675a 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 416f0878de..cfe1d347b0 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 642e76fd1d..67f6daa846 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 97efd428a0..ca7bce4450 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 2cad909a7e..f8ae990d36 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 56e7ed62e9..3ec9ed871d 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 0000000000..5b37bde3ee --- /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 7326d80497..698be68bd7 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 0000000000..4beb11214b --- /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 0000000000..9d464a6d51 --- /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 28999de5e8..2b185cb1b6 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 0000000000..533ba76b6a --- /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 93dfc65bbe..fcbe835e4c 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 0000000000..fdd76cda7e --- /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 39c391eff3..2241ca6644 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 afd59d619e..432c8ae37d 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 0000000000..6eeb898ee1 --- /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 7c0d17c8a8..511f3c9d8e 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 0000000000..d380023855 --- /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 0000000000..c43f434562 --- /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 7659ab4d29..a475518bd8 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 660f639d85..9f91d37c23 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 e52a80be98..ecb061e91c 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 33b6b1d382..9a94e54dd5 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 17757cb9d2..0986b0e024 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 0000000000..72f1ad2a4c --- /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 9f52511a16..8708729bf4 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 0000000000..957a0187c1 --- /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 0000000000..ad7cb55316 --- /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 0000000000..31e41d3f50 --- /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 6749eab129..987d13a394 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 0000000000..291d8cb478 --- /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 0000000000..8a00ac0954 --- /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 b0313a1eee..677e0389d4 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 b0b65a10c8..dedaf02971 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 902e28b208..60a412684f 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 8a8d8b11f5..3741b035d7 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 8b0eb56203..0539f5607b 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 1acaaa53ab..1e1e8897c3 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 99611c5f9b..856dc77a52 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 0000000000..7d1dbad19f --- /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 0000000000..994d02aa7f --- /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 f604594bfc..d5d35ba7c9 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 e67b8f7016..8db6fd4609 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 7052ebfc15..955c4b90cc 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 d635d17894..b01efba8f0 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 049e3a423c..f59f0ba19a 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 7125d3e9ed..297ed6bb86 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 fe5e19f911..c454514912 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 2e15ac8a68..c8b1908258 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 dc0ae1f224..7fda26b290 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 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) 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 @@ LOCK_N_GO_SERVICE_SCHEMA = vol.Schema( } ) -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 @@ class NukiLock(LockDevice): 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 @@ class NukiLock(LockDevice): 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 @@ class NukiLock(LockDevice): 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 932b80690c..e7f078a1a0 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 53bd7fc582..efa38286e7 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 f59f0ba19a..0e432bff8b 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 955c4b90cc..01823882f9 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 b01efba8f0..43a22cb980 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 0e432bff8b..c2be62c232 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 297ed6bb86..b00194b0d9 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 7c88e615fa..29e68a5d7a 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 63bdbf8a92..5c5a095c8f 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 c2be62c232..80b79563e8 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 4c21bb7fde..51f615e68a 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 @@ CONFIG_SCHEMA = vol.Schema( 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 - host = config[DOMAIN].get(CONF_HOST) - password = config[DOMAIN].get(CONF_PASSWORD) + 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() + + 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 2cc46632dd..13784e24d7 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -9,6 +9,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, + CONF_NAME, POWER_WATT, ENERGY_WATT_HOUR, ) @@ -44,6 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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 8bd1952a65..7c13488c3f 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -306,7 +306,7 @@ class Volumio(MediaPlayerDevice): 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 d12ecd9d3a..45f603a2cb 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -26,72 +26,72 @@ TYPE = "type" 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 fda8915829..f0e9b06b76 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 03eedd6d16..9e08a9cff1 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -43,6 +43,7 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" GROUP_NAME_ALL_AUTOMATIONS = "all automations" CONF_ALIAS = "alias" +CONF_DESCRIPTION = "description" CONF_HIDE_ENTITY = "hide_entity" CONF_CONDITION = "condition" @@ -95,6 +96,7 @@ PLATFORM_SCHEMA = vol.Schema( # 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 7ca71fc4f9..17efdba3fb 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -53,7 +53,7 @@ class EditAutomationConfigView(EditIdBasedConfigView): # 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 a668c78598..40bfc4ca0a 100644 --- a/homeassistant/components/device_automation/const.py +++ b/homeassistant/components/device_automation/const.py @@ -4,3 +4,5 @@ CONF_IS_ON = "is_on" 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 722b92e33f..1593e70771 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -8,6 +8,8 @@ from homeassistant.components.device_automation.const import ( 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 @@ ENTITY_TRIGGERS = [ { # 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 @@ TRIGGER_SCHEMA = vol.Schema( 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 460114b143..77b842ba07 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 857b376307..77b842ba07 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 a01dad03d4..b05c04a16f 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 3525f1121c..27b8b860d7 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 3bd29a72a1..1ebe478576 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 873dfee9a9..51bd522f2c 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 ab68d5ba08..7c7efea733 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 dd3463fff9..d73762ce88 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -38,10 +38,12 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 fccfb295c0..b564cff733 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -9,7 +9,12 @@ import voluptuous as vol 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 @@ class SSLCertificate(Entity): """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 f8c99496a5..f44e65512e 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 import config_flow 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 23cc74281e..f28a4f59d8 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -88,7 +88,10 @@ class WithingsFlowHandler(config_entries.ConfigFlow): 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 88b8e6d5ea..1a99abc725 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 93b9a434b7..3ae9d11c3b 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -3,9 +3,7 @@ from aiohttp.web_request import BaseRequest 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 01823882f9..3d5860d0a4 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 43a22cb980..de5c823d99 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 80b79563e8..05a3e89b67 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 b00194b0d9..07fc31ec6e 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 51bd522f2c..873dfee9a9 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 15e50a4012..7f0566ddd4 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 5017af8e57..4cdf3d9042 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 60ccbd9934..225d64be23 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 fcbe835e4c..533c4b3c0f 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 6ab8727440..f0aabef2ae 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 03baf0c032..991222d45b 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 c97565ddfe..6fea704f75 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 5be333cbf1..ed03602375 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 987d13a394..b38928fa75 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 161b5d8559..28ad9ce954 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 2b906dd800..16ce491e77 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 3dfe0265bd..a5a7f320d9 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 @@ class TibberSensorElPrice(Entity): 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 @@ class TibberSensorElPrice(Entity): 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 @@ class TibberSensorElPrice(Entity): @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 @@ class TibberSensorElPrice(Entity): @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 @@ class TibberSensorElPrice(Entity): ]["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 @@ class TibberSensorRT(Entity): 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 @@ class TibberSensorRT(Entity): @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 @@ class TibberSensorRT(Entity): @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 | 186 +++++------ tests/components/deconz/test_light.py | 369 ++++++++++++---------- 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 b82144d37c..bcd408c25a 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -17,7 +17,6 @@ from .gateway import get_gateway_from_config_entry 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 ec1dfd2bcb..bf4b05089a 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -34,7 +34,6 @@ from .gateway import get_gateway_from_config_entry, DeconzEntityHandler 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 @@ class DeconzLight(DeconzDevice, Light): 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 @@ class DeconzGroup(DeconzLight): """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 @@ class DeconzGroup(DeconzLight): "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 b1fd4b10f4..1b51256580 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -10,7 +10,6 @@ from .gateway import get_gateway_from_config_entry 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 2de70f6d24..246c2bae7c 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", + }, + "3": { + "id": "Unsupported cover id", + "name": "Unsupported cover", + "type": "Not a cover", + "state": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", }, } -UNSUPPORTED_COVER = { - "1": { - "id": "Cover id", - "name": "Unsupported switch", - "type": "Not a cover", - "state": {}, - } -} - +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 + level_controllable_cover = hass.states.get("cover.level_controllable_cover") + assert level_controllable_cover.state == "closed" -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 + 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}) -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 + 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 ecce762f51..50a5b2adac 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 @@ GROUP = { }, } -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 ) + 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 -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]) + 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", + }, + ) + + 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"} + ) + + 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_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 +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_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 + tunable_white_light = hass.states.get("light.tunable_white_light") + assert tunable_white_light is not None + light_group = hass.states.get("light.light_group") + assert light_group is None -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_unload_light(hass): - """Test that it works to unload switch entities.""" - gateway = await setup_gateway(hass, {"lights": LIGHT, "groups": GROUP}) - - await gateway.async_reset() - - # 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 6b691bcab8..c574ed8911 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 - -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", + }, + "4": { + "id": "Unsupported switch id", + "name": "Unsupported switch", + "type": "Not a smart plug", + "state": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:03-00", }, } -UNSUPPORTED_SWITCH = { - "1": { - "id": "Switch id", - "name": "Unsupported switch", - "type": "Not a smart plug", - "state": {}, - } -} - +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 + 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 - 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 - ) + on_off_switch = hass.states.get("switch.on_off_switch") + assert on_off_switch.state == "on" + smart_plug = hass.states.get("switch.smart_plug") + assert smart_plug.state == "off" -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]) + warning_device = hass.states.get("switch.warning_device") + assert warning_device.state == "on" + + 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() - assert "switch.name" in gateway.deconz_ids + on_off_switch = hass.states.get("switch.on_off_switch") + assert on_off_switch.state == "off" -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 + warning_device = hass.states.get("switch.warning_device") + assert warning_device.state == "off" + 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}) -async def test_unload_switch(hass): - """Test that it works to unload switch entities.""" - gateway = await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES}) + 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}) - await gateway.async_reset() + 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"}) - assert len(hass.states.async_all()) == 1 + 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 3d5860d0a4..628099a47d 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 de5c823d99..bbeb228f34 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 05a3e89b67..415c8e514f 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 07fc31ec6e..5ff52cf225 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 0635a2d1f1..4fdd513f84 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -282,7 +282,7 @@ class ONVIFHassCamera(Camera): """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 171f25128c..b47cdb9816 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -822,7 +822,7 @@ class YeelightWhiteTempLightsupport: class YeelightWhiteTempWithoutNightlightSwitch( - YeelightGenericLight, YeelightWhiteTempLightsupport + YeelightWhiteTempLightsupport, YeelightGenericLight ): """White temp light, when nightlight switch is not set to light.""" @@ -831,7 +831,7 @@ class YeelightWhiteTempWithoutNightlightSwitch( 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 | 213 +++++++----- tests/components/deconz/test_climate.py | 319 +++++++++++------- tests/components/deconz/test_sensor.py | 301 +++++++++++------ 4 files changed, 527 insertions(+), 310 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 001721d4f0..cc3f3de317 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 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 b6745e1a97..c5c35f1082 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]) + 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 "binary_sensor.name" in gateway.deconz_ids + + presence_sensor = hass.states.get("binary_sensor.presence_sensor") + assert presence_sensor.state == "on" -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]) - await hass.async_block_till_done() +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 + + 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() -async def test_unload_switch(hass): - """Test that it works to unload switch entities.""" - data = {"sensors": SENSOR} - gateway = await setup_gateway(hass, data) + assert "binary_sensor.presence_sensor" in gateway.deconz_ids - await gateway.async_reset() - - assert len(hass.states.async_all()) == 0 + 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 b76b3511a0..1211188d3d 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 @@ SENSOR = { "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 + 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 - gateway.api.sensors["1"].async_update({"state": {"on": False}}) + 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": "auto"}, - blocking=True, - ) - gateway.api.session.put.assert_called_with( - "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"mode": "auto"}' - ) + 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": "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_battery_level = hass.states.get("sensor.thermostat_battery_level") + assert thermostat_battery_level.state == "100" - 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"}' - ) + presence_sensor = hass.states.get("climate.presence_sensor") + assert presence_sensor is None - 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}' - ) + clip_thermostat = hass.states.get("climate.clip_thermostat") + assert clip_thermostat is None - assert len(gateway.api.session.put.mock_calls) == 4 + 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 + + 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}) + assert "climate.thermostat" in gateway.deconz_ids - await gateway.async_reset() - - 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 eb391cc563..947c42e694 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": {}, - }, - "3": { - "id": "Sensor 3 id", - "name": "Sensor 3 name", - "type": "ZHASwitch", - "state": {"buttonevent": 1000}, - "config": {}, - }, - "4": { - "id": "Sensor 4 id", - "name": "Sensor 4 name", - "type": "ZHASwitch", - "state": {"buttonevent": 1000}, - "config": {"battery": 100}, "uniqueid": "00:00:00:00:00:00:00:01-00", }, - "5": { - "id": "Sensor 5 id", - "name": "Sensor 5 name", + "3": { + "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": "Switch 2 id", + "name": "Switch 2", "type": "ZHASwitch", "state": {"buttonevent": 1000}, "config": {"battery": 100}, - "uniqueid": "00:00:00:00:00:00:00:02:00-00", + "uniqueid": "00:00:00:00:00:00:00:03-00", + }, + "5": { + "id": "Daylight sensor id", + "name": "Daylight sensor", + "type": "Daylight", + "state": {"daylight": True, "status": 130}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:04-00", }, "6": { - "id": "Sensor 6 id", - "name": "Sensor 6 name", - "type": "Daylight", - "state": {"daylight": True}, - "config": {}, - }, - "7": { - "id": "Sensor 7 id", - "name": "Sensor 7 name", + "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}}) + 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" + + gateway.api.sensors["1"].async_update({"state": {"lightlevel": 2000}}) gateway.api.sensors["4"].async_update({"config": {"battery": 75}}) + await hass.async_block_till_done() + + light_level_sensor = hass.states.get("sensor.light_level_sensor") + assert light_level_sensor.state == "1.6" + + switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") + assert switch_2_battery_level.state == "75" + + +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 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 - - -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]) - await hass.async_block_till_done() + """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() -async def test_unload_sensor(hass): - """Test that it works to unload sensor entities.""" - gateway = await setup_gateway(hass, {"sensors": SENSOR}) + assert "sensor.light_level_sensor" in gateway.deconz_ids - await gateway.async_reset() - - assert len(hass.states.async_all()) == 0 + 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 0806ba0799..10161856a4 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -88,7 +88,12 @@ class TorqueReceiveDataView(HomeAssistantView): 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 628099a47d..978127c634 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 bbeb228f34..5eeec405e7 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 415c8e514f..9848716d89 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 5ff52cf225..69ca7eefe0 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 129579315a..a4300e1e53 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 0000000000..d6811728a2 --- /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 41c75ec4aa..c536e57714 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 c03ad0f7e1..1e96214237 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 4cdf3d9042..c9b727088a 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 7b266ba741..4ea4a94014 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 fcfc2773ed..2fe1c6b42d 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 225d64be23..3f37de5331 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 533c4b3c0f..6bf91651d2 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 f0aabef2ae..fd30e93171 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 85a117f0b5..2f4d2ca121 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 7277ef5900..e055f67421 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 fdd76cda7e..a7f807e8dc 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 546fea78b6..63954ca83a 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 2241ca6644..008123739d 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 9debeaf416..22a9390957 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 3154e17a50..ba9339c1a9 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 432c8ae37d..bef4f1583b 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 8f5fec9b30..5ac0612946 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 d380023855..cd3c7152c8 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 1e9c089828..40c7c8853d 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 1c24acd96e..ac88ed224e 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 2a10b234e9..4875109694 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 957a0187c1..afc558ca80 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 31e41d3f50..efccc652d5 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 6fea704f75..dbf5e15265 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 ed03602375..391a071cb8 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 b38928fa75..24dbc2cdc1 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 eeffc9262e..4775d62bce 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 c51ce8c6ee..254c09380c 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 2156ea04e0..02c303f932 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 291d8cb478..8e974a0a8d 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 1d8355d215..5e2aa6747a 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 8a00ac0954..adc128991c 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 f564d1424e..c63799bf78 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 1b0658cd17..45a941b665 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 38edfe5a19..89423e071f 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 0607f4ab08..c1a67897b1 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 012aa65187..0eddbe2a15 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 0000000000..8808d85a1d --- /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 0000000000..0b07c5003e --- /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 f907364327..518f006653 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 0000000000..89e0bd818d --- /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 7d1dbad19f..cfccd8b1db 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 a96f8cff52..2f2fdbe9b3 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 3c2f00ba4a..617964e059 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 994d02aa7f..9015f49083 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 1729879a15..3776621bec 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 d9d5e14208..c6c621fbdf 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 160c8a5e45..8e67da86dc 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.const import CONF_USERNAME, CONF_PASSWORD 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 setup_account(account_config: dict, hass, name: str) -> "BMWConnectedDriveAc # 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 33444f1099..ed22be003a 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -13,6 +13,7 @@ import requests 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 @@ class BOMCurrentData: # 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 @@ class BOMCurrentData: _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 @@ class BOMCurrentData: # 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 68f0d77e30..e86ec02040 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -69,7 +69,6 @@ class Concord232Alarm(alarm.AlarmControlPanel): 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 10643f134d..1a406d743b 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.binary_sensor import ( ) 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 @@ class Concord232ZoneSensor(BinarySensorDevice): 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 b81a2193bb..2253f261ad 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 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +import homeassistant.util.dt as dt_util CONDITION_CLASSES = { "cloudy": [], @@ -147,7 +148,7 @@ class DemoWeather(WeatherEntity): @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 c7c488950c..5dd7ab7a88 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.exceptions import PlatformNotReady 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 @@ class DlnaDmrDevice(MediaPlayerDevice): 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 eaae3f1923..457c319d9e 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -8,6 +8,7 @@ import async_timeout 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 @@ class DoorBirdCamera(Camera): 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 643e006dfe..7a0dfa82e7 100644 --- a/homeassistant/components/doorbird/switch.py +++ b/homeassistant/components/doorbird/switch.py @@ -3,6 +3,7 @@ import datetime 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 @@ class DoorBirdSwitch(SwitchDevice): 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 @@ class DoorBirdSwitch(SwitchDevice): 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 ac156e040d..4bc79e7bd3 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -3,6 +3,7 @@ import logging import datetime from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util from .const import DOMAIN @@ -68,9 +69,7 @@ class EbusdSensor(Entity): 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 83e4c089b9..ee74b36962 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 d16ed23266..1e6a051702 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 5805114252..697be4d157 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 @@ class RingLight(Light): 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 @@ class RingLight(Light): """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 @@ class RingLight(Light): 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 cbbecb1a40..413d2a70aa 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 @@ class SirenSwitch(BaseRingSwitch): 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 f2e0ccac2b..cdd6af5761 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA 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 @@ class Season(Entity): """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 @@ class Season(Entity): 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 446a36ec35..ad2072baaa 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 @@ class SystemMonitorSensor(Entity): 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 e5746e088f..b721fa29cd 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 @@ class PerSecondUPnPIGDSensor(UpnpSensor): 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 | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 1385d5e59a..90b4b386f3 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -197,18 +197,20 @@ class GlancesSensor(Entity): elif self.type == "cpu_temp": for sensor in value["sensors"]: if sensor["label"] in [ - "CPU", - "CPU Temperature", - "Package id 0", - "Physical id 0", - "cpu_thermal 1", - "cpu-thermal 1", - "exynos-therm 1", - "soc_thermal 1", - "soc-thermal 1", + "amdgpu 1", "aml_thermal", "Core 0", "Core 1", + "CPU Temperature", + "CPU", + "cpu-thermal 1", + "cpu_thermal 1", + "exynos-therm 1", + "Package id 0", + "Physical id 0", + "radeon 1", + "soc-thermal 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 1ba2c4809b..82db5f6725 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 @@ class PrometheusMetrics: 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 9e313fd369..4ec40731c5 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 d769f797da..aeaa0a62c4 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 @@ from homeassistant.const import ( 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 @@ class AlexaCapibility: "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 708d1592e4..0b5c124376 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 @@ class AlexaFlashBriefingView(http.HomeAssistantView): 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 1e636b96ee..c72101460c 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 @@ from homeassistant.const import ( 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 bfe459b0cf..0d96492e3a 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 69ca7eefe0..87f02e53b7 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 5eeec405e7..0a5cfbfc67 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 9848716d89..a00441518a 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 5ab8d74c64..8be458fc44 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ PROJECT_URLS = { 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 5724d7a3ba..fce0129a7b 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 4e1ffce7e2..9749ab9bb7 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 98fc70f3bf..ce13ca5a59 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -244,8 +244,12 @@ class AiohttpClientMockResponse: 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 ba6b21e002..d847c6df24 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 a00441518a..99e94ec438 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 87f02e53b7..0ac705bf2e 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 69fe3bae61..4c691f66af 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -4,7 +4,9 @@ from copy import deepcopy 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 0d96492e3a..44b27d8e13 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 0ac705bf2e..83ec2d1d2c 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 52e50ab479..3a154b857f 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 0c1cfcf21d..a5b4547b34 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -4,9 +4,11 @@ import asyncio 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 @@ from homeassistant.helpers.event import async_track_time_interval _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 @@ class RealTimeDataEndpoint: 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 99e94ec438..e6ac06e919 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 b6bad10d60..e7706b0435 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 e6ac06e919..e67a7a00c7 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 0a5cfbfc67..746485f2ec 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 e67a7a00c7..8cbf9230ec 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 8be458fc44..e6776d8a1a 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ PROJECT_URLS = { 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 | 215 +++------ 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, 836 insertions(+), 172 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 824fb3828f..0e4199bb09 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 827e05a424..15fcfc1533 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -50,6 +50,7 @@ CONFIG_ENTRY_HANDLERS = { SERVICE_DAIKIN: "daikin", SERVICE_TELLDUSLIVE: "tellduslive", SERVICE_IGD: "upnp", + SERVICE_PLEX: "plex", } SERVICE_HANDLERS = { @@ -69,7 +70,6 @@ SERVICE_HANDLERS = { 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 69e77c8854..665091d69b 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -5,7 +5,7 @@ import plexapi.exceptions 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 @@ from homeassistant.const import ( 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 @@ SERVER_CONFIG_SCHEMA = vol.Schema( 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.""" - - 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": ""}, - ], - ) - - # End of inner functions. - - original_config = config - - hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, CONFIGURING: {}}) - - if hass.data[PLEX_DOMAIN][SERVERS]: - _LOGGER.debug("Plex server already configured") - return False + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}}) plex_config = config.get(PLEX_DOMAIN, {}) - return setup_plex(config=plex_config) + 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, + ) + ) + + +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) + 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 + + _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 0000000000..3c683c802f --- /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 6f19623c80..e77ac303bf 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -14,5 +14,6 @@ PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" 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 0000000000..11c15404f4 --- /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 4269400dc2..94d990952a 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 cfc63948be..bc19ff41df 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -35,26 +35,40 @@ 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, 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. - plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] + 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) + + +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 f469e95da8..7d5b54356a 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -8,21 +8,26 @@ import requests.exceptions 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 962e074996..f41a9bdaba 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 requests import Session 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 @@ class PlexServer: 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 @@ class PlexServer: 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 0000000000..396a3387fe --- /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 7f3f5c1f20..9ddae5acdb 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -45,6 +45,7 @@ FLOWS = [ "openuv", "owntracks", "plaato", + "plex", "point", "ps4", "rainmachine", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83ec2d1d2c..ef8618f146 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 ff2943a583..72fb9ff5a4 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -110,6 +110,7 @@ TEST_REQUIREMENTS = ( "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 0000000000..9c9c00d87a --- /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 0000000000..d027087828 --- /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 0000000000..9c9c1b6252 --- /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 610ec92a2b..83d70830b1 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 8cbf9230ec..99d81158ed 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 ef8618f146..3d0d42cc84 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 0e4199bb09..5d5b0f6c81 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 c454514912..19dd0d5c8b 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 0000000000..5293ad2a1f --- /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 0000000000..7f80fb077c --- /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 0000000000..c932c66627 --- /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 0000000000..eb57a36a2b --- /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 0000000000..4da7bc9e4a --- /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 0000000000..3630c28605 --- /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 0000000000..2f6747ab4c --- /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 0000000000..7cb14b03c6 --- /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 9ddae5acdb..9a534c01bb 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -30,6 +30,7 @@ FLOWS = [ "ios", "ipma", "iqvia", + "izone", "life360", "lifx", "linky", diff --git a/requirements_all.txt b/requirements_all.txt index 99d81158ed..ae4007ee77 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 3d0d42cc84..f964405858 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 72fb9ff5a4..384d50bcce 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -145,6 +145,7 @@ TEST_REQUIREMENTS = ( "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 0000000000..1baeb3fee8 --- /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 0000000000..faa920271e --- /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 20652ddd04..feda5da732 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 ae4007ee77..064762591d 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 f964405858..2f5a1bef6e 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 19dd0d5c8b..9766f01188 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 fd74d9831f..131d1a23b6 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 c2834fbc78..83805692e4 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 4ff59ac177..fb38b41f31 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 609ea72cc6..99ca3cb1cc 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 8c231ec63e..a08c0a59c0 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 5d5b0f6c81..a29586c7b6 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 9766f01188..9bcd475d5d 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 0000000000..d007b2d3df --- /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 0000000000..54837b2eb0 --- /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 0000000000..340291807e --- /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 064762591d..437aa60cf9 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 0000000000..fe6b0db52d --- /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 0000000000..50d945e7fa --- /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 0000000000..c5e4857297 --- /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 | 102 +++--- tests/components/deconz/test_gateway.py | 293 +++++++++--------- tests/components/deconz/test_light.py | 51 +-- tests/components/deconz/test_scene.py | 95 ++---- tests/components/deconz/test_sensor.py | 52 +--- tests/components/deconz/test_services.py | 82 ++--- tests/components/deconz/test_switch.py | 52 +--- 12 files changed, 305 insertions(+), 585 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index a090dca0d0..75898b0fda 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -184,11 +184,7 @@ class DeconzGateway: 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 @@ class DeconzGateway: 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 8d27d386da..a84e799d44 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -9,7 +9,6 @@ from .gateway import get_gateway_from_config_entry 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 c5c35f1082..2f42193291 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 @@ SENSORS = { }, } -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 1211188d3d..cee91f00c4 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -3,12 +3,13 @@ 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.climate as climate +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration + SENSORS = { "1": { "id": "Thermostat id", @@ -42,50 +43,6 @@ SENSORS = { }, } -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 246c2bae7c..5c7ee48a78 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -3,12 +3,13 @@ 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.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 @@ COVERS = { }, } -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 72966ba6c6..ade9aa02ad 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 copy import deepcopy -from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT, DeconzEvent -from homeassistant.core import callback +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_create_event(hass): - """Successfully created a deCONZ event.""" - mock_remote = Mock() - mock_remote.name = "Name" +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 - mock_gateway = Mock() - mock_gateway.hass = hass + switch_1 = hass.states.get("sensor.switch_1") + assert switch_1 is None - event = DeconzEvent(mock_remote, mock_gateway) + switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level") + assert switch_1_battery_level is None - assert event.event_id == "name" + 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" -async def test_update_event(hass): - """Successfully update a deCONZ event.""" - mock_remote = Mock() - mock_remote.name = "Name" + mock_listener = Mock() + unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) - 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() + 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 d84706430f..25a1cd465c 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(): +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].get(config[deconz.CONF_BRIDGEID]) + + +async def test_gateway_setup(hass): """Successful setup.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - api = Mock() - api.async_add_remote.return_value = Mock() - api.sensors = {} + 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 - deconz_gateway = gateway.DeconzGateway(hass, entry) + assert len(gateway.deconz_ids) == 0 + assert len(hass.states.async_all()) == 0 - 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", - ) - assert len(api.start.mock_calls) == 1 + 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(): +async def test_gateway_retry(hass): """Retry setup.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - - deconz_gateway = gateway.DeconzGateway(hass, entry) - - with patch.object( - gateway, "get_gateway", side_effect=errors.CannotConnect + data = deepcopy(DECONZ_WEB_REQUEST) + with patch( + "homeassistant.components.deconz.gateway.get_gateway", + side_effect=deconz.errors.CannotConnect, ), pytest.raises(ConfigEntryNotReady): - await deconz_gateway.async_setup() + await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) -async def test_gateway_setup_fails(): +async def test_gateway_setup_fails(hass): """Retry setup.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - - deconz_gateway = gateway.DeconzGateway(hass, entry) - - with patch.object(gateway, "get_gateway", side_effect=Exception): - result = await deconz_gateway.async_setup() - - assert not result + 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 + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) - deconz_gateway = gateway.DeconzGateway(hass, entry) - with patch.object(gateway, "async_dispatcher_send") as mock_dispatch_send: - deconz_gateway.async_connection_status_callback(True) + event_call = Mock() + unsub = async_dispatcher_connect(hass, gateway.signal_reachable, event_call) - await hass.async_block_till_done() - assert len(mock_dispatch_send.mock_calls) == 1 - assert len(mock_dispatch_send.mock_calls[0]) == 3 + gateway.async_connection_status_callback(False) + await hass.async_block_till_done() + + assert gateway.available is False + assert len(event_call.mock_calls) == 1 + + unsub() -async def test_add_device(hass): - """Successful retry setup.""" - entry = Mock() - entry.data = ENTRY_CONFIG +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" - 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.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() - await hass.async_block_till_done() - assert len(mock_dispatch_send.mock_calls) == 1 - assert len(mock_dispatch_send.mock_calls[0]) == 3 + assert gateway.api.host == "2.3.4.5" -async def test_shutdown(): - """Successful shutdown.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG +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 + ) - deconz_gateway = gateway.DeconzGateway(hass, entry) - deconz_gateway.api = Mock() - deconz_gateway.shutdown(None) + result = await gateway.async_reset() + await hass.async_block_till_done() - assert len(deconz_gateway.api.close.mock_calls) == 1 - - -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 = {} - - 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 - - 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"} - - hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) - assert await deconz_gateway.async_reset() is True - - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 7 - - assert len(listener.mock_calls) == 1 - assert len(deconz_gateway.listeners) == 0 - - assert len(event.async_will_remove_from_hass.mock_calls) == 1 - assert len(deconz_gateway.events) == 0 - - 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 50a5b2adac..14dc5cc8ea 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -3,12 +3,13 @@ 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.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 @@ LIGHTS = { }, } -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 074e943548..dcc8ba500c 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 - 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 + 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 -async def test_unload_scene(hass): - """Test that it works to unload scene entities.""" - gateway = await setup_gateway(hass, {"groups": GROUP}) + light_group_scene = hass.states.get("scene.light_group_scene") + assert light_group_scene + + group_scene = gateway.api.groups["1"].scenes["1"] + + 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 947c42e694..928e527dd0 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 @@ SENSORS = { }, } -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 63934871fc..533d85eef7 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 @@ SENSOR = { } -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 c574ed8911..262bd7001f 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -3,13 +3,13 @@ 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.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 @@ SWITCHES = { }, } -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 978127c634..18c19a9012 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 746485f2ec..900bfddda2 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 437aa60cf9..29897b5622 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 2f5a1bef6e..122ff317c0 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 0000000000..5498624a06 --- /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 0000000000..2ada5e810e --- /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 0000000000..2e77b4ba97 --- /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 254c09380c..ec742e4113 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 c63799bf78..921048286b 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 89423e071f..f1b851b05b 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 c1a67897b1..517d48354d 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 5bf342836c..51276869ec 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 1643ecb148..3c345a1a78 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 d0fcb6a527..71934516ea 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 30a77102d0..9e408eb0d5 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 a29586c7b6..5d5b0f6c81 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 9bcd475d5d..9766f01188 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 d007b2d3df..0000000000 --- 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 54837b2eb0..0000000000 --- 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 340291807e..0000000000 --- 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 29897b5622..ac49c41539 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 fe6b0db52d..0000000000 --- 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 50d945e7fa..0000000000 --- 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 c5e4857297..0000000000 --- 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 153f6b5aea..dffb4b5266 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 ac49c41539..522f06bca0 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 9e08a9cff1..f0529f126f 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -3,6 +3,7 @@ import asyncio from functools import partial import importlib import logging +from typing import Any import voluptuous as vol @@ -34,7 +35,7 @@ from homeassistant.loader import bind_hass 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 @@ class AutomationEntity(ToggleEntity, RestoreEntity): 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 d491765bb0..8d2b4430fe 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 @@ from homeassistant.const import ( ) -# 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 @@ class CoverDevice(Entity): """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 @@ class CoverDevice(Entity): """ 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 @@ class CoverDevice(Entity): """ 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 @@ class CoverDevice(Entity): """ 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 @@ class CoverDevice(Entity): """ 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 @@ class CoverDevice(Entity): """ 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 7298ce8c1d..8ef662ec87 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -4,6 +4,7 @@ import logging 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 homeassistant.loader import bind_hass 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 @@ class IndexView(web_urldispatcher.AbstractResource): """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 @@ class IndexView(web_urldispatcher.AbstractResource): 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 d8fa8853c7..7d1e24f369 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -18,7 +18,7 @@ from homeassistant.util.yaml import dump 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 @@ class IpBan: 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 4eba4657d9..dac08afe47 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -36,7 +36,7 @@ from .const import ( ) -# 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 2027a8fc45..8f3b5d87f8 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.const import ( ) 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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 @@ class LightSwitch(Light): @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 e53954a65d..952fa41c42 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -52,7 +52,7 @@ from homeassistant.helpers.logging import KeywordStyleAdapter 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 has_at_least_one_key(*keys: str) -> Callable: 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 entity_domain(domain: 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 @@ time_period_dict = vol.All( ) -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 deprecated( ) ) - 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 0b569e2d4a..23728b6510 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -4,7 +4,7 @@ import logging 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 @@ import homeassistant.util.dt as date_util 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 98e3849bfb..9af1998e89 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -7,7 +7,7 @@ import random 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.const import ( 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 @@ class Template: """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 @@ class Template: ).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 @@ class Template: @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 0a9bac3018..00f5984c58 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -5,7 +5,7 @@ import importlib 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.requirements import pip_kwargs 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 5d5b0f6c81..a29586c7b6 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 9766f01188..9bcd475d5d 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 0000000000..d007b2d3df --- /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 0000000000..6c633f848c --- /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 0000000000..340291807e --- /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 522f06bca0..f59ed13bda 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 0000000000..fe6b0db52d --- /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 0000000000..50d945e7fa --- /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 0000000000..c5e4857297 --- /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 be079e83fa..ff9f27d484 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -19,9 +19,16 @@ from .core.const import ( 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 @@ from .core.const import ( 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_SET_ZIGBEE_CLUSTER_ATTRIBUTE = "set_zigbee_cluster_attribute" 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 @@ SERVICE_SCHEMAS = { 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 @@ def async_load_api(hass): 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 cd407cfc41..25c11a9fd4 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -13,7 +13,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send 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 c35cb168fd..ac83c2cdcd 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -34,6 +34,11 @@ ATTR_RSSI = "rssi" 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_DOORLOCK = "door_lock" 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 @@ UNKNOWN = "unknown" 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 ffd5aa2147..d279af4633 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 6d13f7adbf..189b936510 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 f59ed13bda..e66f248cbb 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 122ff317c0..c254da1f9d 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 8a03ac4740..cf26955b20 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 e66f248cbb..9d7e8b645b 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 c254da1f9d..fdc9ad8dc3 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 0000000000..2eca398d99 --- /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 0000000000..d1b514ea93 --- /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 0000000000..cf66bb4e2a --- /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 0000000000..d99cbe8026 --- /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 0000000000..352d1da206 --- /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 0000000000..f7b3f56f2e --- /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 0000000000..83fe922d8c --- /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 0000000000..356c7857d9 --- /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 0000000000..c05141ff0b --- /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 0000000000..e8a1c494d4 --- /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 0000000000..a99a32bb95 --- /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 0000000000..7c1e141eef --- /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 0000000000..0f29967b28 --- /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 0000000000..081b6d8660 --- /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 0000000000..7735f497f8 --- /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 e58c901f36..f8f64d5426 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 d6811728a2..9620526e36 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 e095cc360a..73e899106c 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 0000000000..07affc990a --- /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 c536e57714..1a03143f11 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 92fd1e3e74..612c5afd03 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 0000000000..2499befecb --- /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 0000000000..3786b03f41 --- /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 8d064622f7..d9fdc8f91c 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 7a6e9a8a91..b528cdeb70 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 0000000000..b80d9bee4e --- /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 0000000000..c90416b061 --- /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 0000000000..69b8ce8a31 --- /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 0000000000..c6e075683a --- /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 0000000000..fcd5c1df01 --- /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 0000000000..4f90cf71cb --- /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 0000000000..7e632c8dd6 --- /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 bfed5937e2..3af9ab0072 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 0000000000..b450a3cbdb --- /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 6ebaa08457..9a3ef350ab 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 9565bb6661..9027347174 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 f43fbeb58b..c7e89c368c 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 0000000000..8d5c25babf --- /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 0000000000..eb4f6459f4 --- /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 0000000000..58a5169ac0 --- /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 0000000000..d2610c68ae --- /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 0000000000..130cf2067a --- /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 0000000000..8ac90efe3d --- /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 0000000000..606f97d696 --- /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 0000000000..46cd613df4 --- /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 4775d62bce..807b85c5fb 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 adc128991c..3469079f23 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 921048286b..199b150f68 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 45a941b665..b769e56c97 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 0000000000..248e8f9f44 --- /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 0b07c5003e..b6f10842b4 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 0000000000..80301cfd57 --- /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 3bef273b83..05b0ffc0c4 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 89e0bd818d..f38a74e5c1 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 0000000000..7b2bc3b028 --- /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 0000000000..caa00f36c8 --- /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 b66786cc9e..ad715d54eb 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 9015f49083..5ca969f039 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 22d8884d66..d32c9640fd 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 0000000000..c7485b0924 --- /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 ea3e801ac5..ebb0895bd7 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 9d7e8b645b..18ca056584 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 fdc9ad8dc3..621863c8ef 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 b0e5fdb208..bad90d9e82 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 18ca056584..ef036c99c7 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 621863c8ef..a8aa92c81d 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 436d25750f..0e450f0623 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 9bcd475d5d..700e714583 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 5fb0701198..291c774c38 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 8586d950e3..4fc62e4492 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 @@ UPDATE_DELAY = timedelta(seconds=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 @@ SENSOR_TYPES = { 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 @@ class SolarEdgeSensor(Entity): @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 @@ class SolarEdgeData: 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 ef036c99c7..58c59a1c04 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 4617fcf054..bcf398464b 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 @@ from homeassistant.core import callback 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 @@ CONF_OFF_DELAY = "off_delay" 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 @@ class MqttBinarySensor( 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 @@ class MqttBinarySensor( 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 @@ class MqttBinarySensor( 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 @@ class MqttBinarySensor( 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 af27ff8c7d..28f1a7e972 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 @@ from tests.common import ( ) +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 900bfddda2..32804d7904 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 58c59a1c04..8af0da567d 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 e6776d8a1a..87704a3c6a 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ REQUIRES = [ "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 dec3e78a50..6040c8655b 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 313a6337a1..09cf40f193 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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 @@ class WhoisSensor(Entity): 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 8af0da567d..c67928093c 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 ba44d86c2e..850eae7604 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -139,6 +139,7 @@ class Doods(ImageProcessingEntity): 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 @@ class Doods(ImageProcessingEntity): # 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 3e1ce22a23..75c1bd3dcd 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 c67928093c..a17c201a95 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 700e714583..abd3379221 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 2d19ab25fe..7fe8bba687 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 607d9c4553..ea5a64d97e 100644 --- a/homeassistant/components/spaceapi/__init__.py +++ b/homeassistant/components/spaceapi/__init__.py @@ -7,9 +7,7 @@ from homeassistant.components.http import HomeAssistantView 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 @@ import homeassistant.util.dt as dt_util _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_ICONS = "icons" 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 @@ CONFIG_SCHEMA = vol.Schema( 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 @@ class APISpaceApiView(HomeAssistantView): 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 @@ class APISpaceApiView(HomeAssistantView): 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 02a6eccc28..58c417831a 100644 --- a/tests/components/spaceapi/test_init.py +++ b/tests/components/spaceapi/test_init.py @@ -25,6 +25,34 @@ CONFIG = { "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 e455d5581d..1205521706 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -74,7 +74,7 @@ class EsphomeLight(EsphomeEntity, Light): 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 d63d1707fa..2df50699a9 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 a17c201a95..b4ea5b509d 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 a8aa92c81d..214f2ee30e 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 32804d7904..842cf4840c 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 b4ea5b509d..42082b76ae 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 87704a3c6a..26f112bb00 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ REQUIRES = [ # 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 44b27d8e13..e697164a35 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 214f2ee30e..e3df009903 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 e35be24fc8..0547628b4b 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 42082b76ae..1e2c466a75 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 7ecc298e3f..be7f0a524d 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 1e2c466a75..4baf0de152 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 e697164a35..b9b919c4bf 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 e3df009903..6b4d7fbc08 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 bece55ae09..451a6f3e33 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 4baf0de152..08ed0d4476 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 eb006f408b..1ffbe69888 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 ac334587b8..f16758a535 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -3,6 +3,8 @@ import logging 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 08ed0d4476..ad7ba89e4c 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 a1a35e9f3b..44cd95e1f5 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 0000000000..c609c2eb5d --- /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 0000000000..109a2b1fd4 --- /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 0000000000..91124d47f4 --- /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 0000000000..5052b8e47f --- /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 bf136731cb..012c1e1df3 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 @@ class VivotekCam(Camera): 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 @@ class VivotekCam(Camera): @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 8a6a37762d..cce2307bc4 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 ad7ba89e4c..939fcc2797 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 a29586c7b6..302ff94655 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 87b073db05..bca91134be 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -131,6 +131,9 @@ async def async_setup_entry(hass, entry): 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 0000000000..3dea978044 --- /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 97fdfd9d36..615899a98c 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 @@ from homeassistant.components.light import ( 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_HUE = "hue" 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 @@ class TradfriGroup(Light): @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 @@ class TradfriLight(Light): @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 627a988215..4877dbbb54 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 @@ class TradfriDevice(Entity): @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 4be72eb735..545c1ad93c 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 @@ class TradfriSwitch(SwitchDevice): @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 665091d69b..dd458dda07 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 @@ from .const import ( CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, CONF_SERVER, + CONF_SERVER_IDENTIFIER, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, @@ -28,6 +30,7 @@ from .const import ( PLATFORMS, PLEX_MEDIA_PLAYER_OPTIONS, PLEX_SERVER_CONFIG, + REFRESH_LISTENERS, SERVERS, ) from .server import PlexServer @@ -61,7 +64,7 @@ _LOGGER = logging.getLogger(__package__) 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 e77ac303bf..478dd3754e 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -7,6 +7,7 @@ DEFAULT_SSL = False 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 bc19ff41df..4d097253ea 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -39,6 +39,7 @@ from .const import ( 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 302ff94655..65ee6e9e25 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 abd3379221..640a9a7bcc 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 0000000000..8c61ad5418 --- /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 0000000000..4dfe04f9c2 --- /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 0000000000..0c2d6d9366 --- /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 0000000000..7e23edb125 --- /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 0000000000..926f73fa4d --- /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 0000000000..4ff6435b64 --- /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 939fcc2797..250643f745 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 3c683c802f..e620e4869e 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -6,13 +6,22 @@ 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.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 .const import ( # pylint: disable=unused-import 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 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) + 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 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # 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 396a3387fe..c093d4fe0c 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 9c9c1b6252..e98aed793c 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -4,7 +4,14 @@ 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 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 65ee6e9e25..a4f52af76c 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 640a9a7bcc..8fe4703591 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 0000000000..860c7d4dcb --- /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 0000000000..42b58e7f50 --- /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 0000000000..066f3270cc --- /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 0000000000..2a2f50532b --- /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 0000000000..5f4f2defe3 --- /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 250643f745..614362578e 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 567d0b95ff..7bb124ae54 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 0000000000..6379df936b --- /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 0000000000..5a1916bce5 --- /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 7a93c6ff9c..3968c1f00c 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 612c5afd03..558fd9e589 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 0000000000..7448100158 --- /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 008123739d..785e9ca291 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 ba9339c1a9..a6a7994b7c 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 00e3337dfe..8e9b3272f9 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 2ada5e810e..7fa9f62be0 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 0000000000..c79a49470e --- /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 58b6e1e63f..c87c1a702c 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 b769e56c97..cd5cbc0d6a 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 d311b3b0d3..090de51703 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 37dcd8ac22..a033d2954d 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 068f434154..c21a47c7ea 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 a1614eb1ce..21c911a66c 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 | 184 ++++++++++++++---- 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, 424 insertions(+), 158 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 d1b514ea93..93bcc5aba4 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) - generate.generate(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" - print("Running hassfest to pick up new codeowner and config flow.") + generate.generate(template, info) + + 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 0000000000..54a182be31 --- /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 d99cbe8026..75a869572f 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 352d1da206..a7263daaf4 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 @@ 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(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_info() -> Info: +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 f7b3f56f2e..6bccf6529f 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_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) +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 83fe922d8c..68ab771122 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 c05141ff0b..e08851f47a 100644 --- a/script/scaffold/templates/integration/config_flow.py +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -3,10 +3,9 @@ import logging 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 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 7735f497f8..35d8a96ab2 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 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 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 0000000000..16d13aaa99 --- /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 356c7857d9..0000000000 --- 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 a99a32bb95..0000000000 --- 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 0000000000..7ab8b73678 --- /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 7c1e141eef..cb4ecac61f 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 0f29967b28..0000000000 --- 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 081b6d8660..0000000000 --- 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 a4f52af76c..38e3917734 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 690d3102f9..0000000000 --- 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 98db00c309..0000000000 --- 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 cfe35a9a63..0000000000 --- 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 614362578e..0ca24c8e9d 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 38e3917734..06177f069c 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 61da78fa6d..0000000000 --- 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 3141314b04..0000000000 --- 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 9e2f8886d3..0000000000 --- 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 7e26e6c9e5..0000000000 --- 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 0ca24c8e9d..0a6f1979a9 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 06177f069c..b6bdcc2704 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 8fe4703591..ae072cd092 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 5243fe379a..0000000000 --- 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 c1453d88d8..0000000000 --- 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 b7c94933a3..0000000000 --- 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 0a6f1979a9..a3c1548bc1 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 ae072cd092..7e05cdf0b3 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 0000000000..9a5c8ec32a --- /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 0000000000..e26e2e1d6e --- /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 0000000000..ba4908fe85 --- /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 a3c1548bc1..bf217688d2 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 6b4d7fbc08..9e846c9c41 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 384d50bcce..d74a57d678 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -87,6 +87,7 @@ TEST_REQUIREMENTS = ( "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 0000000000..ac0ec70965 --- /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 0000000000..783209690a --- /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 0000000000..9b682f6c51 --- /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 0000000000..a3af39129d --- /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 0000000000..08da738f04 --- /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 0000000000..bda8454f3f --- /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 0000000000..765c438c1c --- /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 0000000000..07881e8bd3 --- /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 0000000000..149b4d06c3 --- /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 0000000000..52df0d4eb3 --- /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 0000000000..81fb246178 --- /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 0000000000..a776fa91c4 --- /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 0000000000..a302d56490 --- /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 b6bdcc2704..256a6b3e41 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 71e04d7b8c..0000000000 --- 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 050a78223c..0000000000 --- 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 f1d1787b7b..0000000000 --- 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 bf217688d2..011e8b7a09 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 9e846c9c41..c0e94a5afe 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 2a278cf1d3..0000000000 --- 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 e33902c3fd..0000000000 --- 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 256a6b3e41..9fe3e10c8b 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 d685ab5037..0000000000 --- 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 b34a8b8383..0000000000 --- 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 2f499e52e2..0000000000 --- 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 011e8b7a09..3214c5e43a 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 1a2f642f91..935ebb9b49 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -94,7 +94,9 @@ class HMThermostat(HMDevice, ClimateDevice): 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 @@ class HMThermostat(HMDevice, ClimateDevice): """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 c2273c5407..fb388afac0 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -25,8 +25,6 @@ from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) -DEFAULT_DURATION = 5 - CAPABILITIES_COLOR_LOOP = 0x4 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 @@ -91,6 +89,7 @@ class Light(ZhaEntity, light.Light): 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 @@ class Light(ZhaEntity, light.Light): @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 @@ class Light(ZhaEntity, light.Light): 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 @@ class Light(ZhaEntity, light.Light): 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 @@ class Light(ZhaEntity, light.Light): 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 @@ class Light(ZhaEntity, light.Light): 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 @@ class Light(ZhaEntity, light.Light): 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 @@ class Light(ZhaEntity, light.Light): """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 @@ class Light(ZhaEntity, light.Light): 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 2b167d52ac..3101abc526 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 a6a754fc2a..01c5c12efb 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -22,7 +22,7 @@ from . import ( 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 d6d901ac3b..4e417fca21 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -16,7 +16,7 @@ from . import ( 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 cea246af32..112fca2419 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 3214c5e43a..07dd2c3965 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 c0e94a5afe..7ece1a030a 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 9fe3e10c8b..f8c632f705 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 5898aa36e9..0000000000 --- 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 d40de718f9..0000000000 --- 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 31fafe17ed..0000000000 --- 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 07dd2c3965..c8f95e2970 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 f8c632f705..88fdbd45f9 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 be09312f69..d2f842956d 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -310,7 +310,7 @@ class ZHAGateway: @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 75ef8cce19..d648390260 100644 --- a/homeassistant/components/zha/core/patches.py +++ b/homeassistant/components/zha/core/patches.py @@ -9,9 +9,7 @@ https://home-assistant.io/components/zha/ 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 apply_application_controller_patch(zha_gateway): 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 e78661a04e..7744b9f223 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 c8f95e2970..c9b28ae1a1 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 7ece1a030a..333c644ebf 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 7ba7d94d25..47f81787ac 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 b4c313041f..6a7638d9f8 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 a8c55890ac..3fe5e7937c 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 3101abc526..08c6cfe18c 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 49a60a0f76..7381b55731 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 dc3ea35229..faa44f3492 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 07d5bd8ca7..ac6bc73b80 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 4fdd513f84..0c11656878 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -156,7 +156,7 @@ class ONVIFHassCamera(Camera): 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 @@ class ONVIFHassCamera(Camera): 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 2a041ce668..b83358822b 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 c9b28ae1a1..62218b1478 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 333c644ebf..b824054a93 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 896867fcb1..605c7f9d7e 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 605c7f9d7e..e50989a15d 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 842cf4840c..47325a6c93 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 62218b1478..04b6eaeb3e 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 b824054a93..b962ca1280 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 7bb124ae54..e84de4ffc2 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 0000000000..434c236418 --- /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 0000000000..438822a97f --- /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 0000000000..7d73cb8d4a --- /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 0000000000..9dc3d88322 --- /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 03c128f636..c68f49509b 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 eb4f6459f4..4c24dddbe8 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 0000000000..ea680a638e --- /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 8ac90efe3d..b58cdfe728 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 0000000000..537bd5e3fa --- /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 46cd613df4..b906d0d8dc 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 fe93407b42..b539e315ea 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 5ff04c5ee7..489e66c2b1 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -145,8 +145,8 @@ class LinkySensor(Entity): 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 96d541b195..28a4e853f2 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -24,6 +24,8 @@ ATTR_TO_HA_METRIC = { "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 @@ ATTR_TO_HA_IMPERIAL = { "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 93bcc5aba4..22cdee8f69 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -4,7 +4,7 @@ from pathlib import Path 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 54a182be31..801b8ebb5f 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -18,5 +18,16 @@ https://developers.home-assistant.io/docs/en/creating_integration_file_structure 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 0000000000..3449009818 --- /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 0000000000..ff15625ad7 --- /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 49f738cf96..4c9c892b93 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 850eae7604..3eec85b3e5 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -14,6 +14,7 @@ from homeassistant.components.image_processing import ( CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, + draw_box, ) from homeassistant.core import split_entity_id from homeassistant.helpers import template @@ -68,24 +69,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -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 b1c167a417..e9621fe6bb 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 import Entity 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 = cv.PLATFORM_SCHEMA.extend( 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 e675d18a00..f3a7121c0b 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 b977a087ea..65e20f558a 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -12,6 +12,7 @@ from homeassistant.components.image_processing import ( CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, + draw_box, ) from homeassistant.core import split_entity_id from homeassistant.helpers import template @@ -67,24 +68,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -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 9419cbaaef..279ac3b103 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 04b6eaeb3e..037fb1fcd2 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 b962ca1280..63ad27a654 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 d74a57d678..649c48e1b7 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -111,6 +111,7 @@ TEST_REQUIREMENTS = ( "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 88fdbd45f9..a4d6d0d201 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 13ade253ff..b609773741 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 4c9ed56352..04d3844162 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 c6258e244e..362424c7fa 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 @@ ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh" 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 037fb1fcd2..b8d4a15865 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 0000000000..e6cf69341f --- /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 b77528e0c3..a876819373 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -24,10 +24,12 @@ from homeassistant.const import ( 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 @@ SENSOR_SCHEMA = vol.Schema( 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 @@ class SensorTemplate(Entity): state_template, icon_template, entity_picture_template, + availability_template, entity_ids, device_class, attribute_templates, @@ -162,10 +165,12 @@ class SensorTemplate(Entity): 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 @@ class SensorTemplate(Entity): """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 @@ class SensorTemplate(Entity): """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 @@ class SensorTemplate(Entity): 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 @@ class SensorTemplate(Entity): 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 9223399bee..b3813da176 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -3,6 +3,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START 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 @@ class TestTemplateSensor: 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 @@ class TestTemplateSensor: 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 f0da251f5e..6a819fbc16 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 aed12bc65a..3d4a03fb0a 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -240,6 +240,8 @@ class ZigbeeChannel(LogMixin): { "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 1db4aafeeb..82d20ff78c 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -187,6 +187,13 @@ class ZHADevice(LogMixin): """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 0000000000..6a96ce5aa3 --- /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 e1ed6a678e..cfc32a020c 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 0000000000..9de04ae8e6 --- /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 5492582ebe..67724e9fcf 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 a4b9bc5cd7..af9375aad0 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 727ec91dc3..324dcb019b 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 6da26784d9..3c753d832e 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -189,17 +189,19 @@ class MVGLiveData: 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 023fb32e6e..9ec8c56d77 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -264,8 +264,7 @@ class OnkyoDevice(MediaPlayerDevice): 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 @@ class OnkyoDeviceZone(OnkyoDevice): 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 9d34cc6fb7..b36e0a34fa 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -320,10 +320,10 @@ class Recorder(threading.Thread): 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 7d2f51f29a..75aec037a2 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -484,39 +484,44 @@ class TodoistProjectData: 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 1da70bc60e..913d193845 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -396,10 +396,12 @@ class LgWebOSDevice(MediaPlayerDevice): 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 108fcbc774..9a1f375fdf 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -165,7 +165,7 @@ class WebSocketHandler: 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 5a5ffb34ab..80642a373d 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 223ce810d7..841b283a98 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -851,7 +851,8 @@ async def async_setup_entry(hass, config_entry): # 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 async_setup_entry(hass, config_entry): "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 dbfb9ae186..4c1a9803d7 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 00f5984c58..ecac61895c 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 c29d41ace9..31761f2560 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 @@ class State: ) 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 @@ class StateMachine: 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 @@ class StateMachine: 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 af8d5589c8..4911c5d5fb 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -3,7 +3,7 @@ from datetime import timedelta 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 @@ from homeassistant.helpers.entity_registry import ( 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 @@ class Entity: 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 @@ class Entity: 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 @@ class Entity: 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 @@ class Entity: 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 @@ class Entity: 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 @@ class Entity: # 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 @@ class Entity: 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 4911c5d5fb..fad02dee07 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -231,7 +231,11 @@ class Entity: @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 f0529f126f..f669d41585 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -3,7 +3,7 @@ import asyncio 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 @@ from homeassistant.const import ( 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.config_validation import ENTITY_SERVICE_SCHEMA 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 @@ SERVICE_TRIGGER = "trigger" _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 b090484ab6..fe2d65edef 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -13,8 +13,8 @@ TRIGGER_SCHEMA = vol.Schema( ) -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 d372aedd1d..26dacac974 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -24,7 +24,9 @@ TRIGGER_SCHEMA = vol.Schema( ) -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 @@ async def async_trigger(hass, config, action, automation_info): 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 3f2aa1c00d..0ef0884d32 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 bd1da7e7e1..e4eb029d5a 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -21,7 +21,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -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 7bc4c93776..9512db8261 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -32,7 +32,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -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 fd9a778dbf..135a421f72 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -25,7 +25,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -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 b33d724d77..9dd4657291 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -40,7 +40,7 @@ TRIGGER_SCHEMA = vol.All( _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 5fbe97185a..184b9ea302 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -37,7 +37,9 @@ TRIGGER_SCHEMA = vol.All( ) -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 @@ async def async_trigger(hass, config, action, automation_info): 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 7cbbe56f32..66892784a5 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -28,7 +28,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -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 c83d660912..f2b4134de4 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -28,7 +28,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema( ) -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 3942d0efad..231bc346e1 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -18,7 +18,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -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 f749a308bf..ee09291611 100644 --- a/homeassistant/components/automation/time_pattern.py +++ b/homeassistant/components/automation/time_pattern.py @@ -30,7 +30,7 @@ TRIGGER_SCHEMA = vol.All( ) -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 706afbe904..bbcf9bd9dd 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 35b1100602..535ef298a2 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -31,7 +31,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -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 c609c2eb5d..0000000000 --- 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 0000000000..70b79becb8 --- /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 0000000000..2211b30010 --- /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 28f36b8f43..77efc78562 100644 --- a/homeassistant/components/deconz/device_automation.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -3,6 +3,7 @@ import voluptuous as vol 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 @@ REMOTES = { 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 9508dd9c84..b444abd523 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 @@ DOMAIN = "device_automation" _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 1593e70771..b7cadd1349 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 @@ from homeassistant.components.device_automation.const import ( 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 @@ ENTITY_TRIGGERS = [ }, ] -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 0000000000..ea37b8e947 --- /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 61292d4744..0000000000 --- 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 0000000000..a69ca7ab8f --- /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 0000000000..f2a82afdc2 --- /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 0000000000..ca91cc7051 --- /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 61292d4744..0000000000 --- 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 0000000000..032c765bf5 --- /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 0000000000..9be294d546 --- /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 6a96ce5aa3..46e3beafca 100644 --- a/homeassistant/components/zha/device_automation.py +++ b/homeassistant/components/zha/device_trigger.py @@ -6,6 +6,7 @@ from homeassistant.components.device_automation.exceptions import ( 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 = "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, - } - ) +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 133251e779..afb8c3934a 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -8,16 +8,14 @@ from typing import Callable, Container, Optional, Union, cast 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 @@ ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config" _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 @@ async def async_and_from_config( 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 zone_from_config( 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 952fa41c42..113f2437ce 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -827,11 +827,16 @@ OR_CONDITION_SCHEMA = vol.Schema( } ) -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 @@ _SCRIPT_WAIT_TEMPLATE_SCHEMA = vol.Schema( } ) -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 23728b6510..14ff873d4d 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -336,7 +336,7 @@ class Script: 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 fda5c74322..bc39b1f5e0 100644 --- a/tests/common.py +++ b/tests/common.py @@ -54,7 +54,9 @@ 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.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 91124d47f4..0000000000 --- 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 0000000000..b5502d8fe3 --- /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 0000000000..5be354c78f --- /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 0be566d4b5..6590028d76 100644 --- a/tests/components/deconz/test_device_automation.py +++ b/tests/components/deconz/test_device_trigger.py @@ -3,9 +3,9 @@ from asynctest import patch 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_SENSOR = { 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 0000000000..bb50778db5 --- /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 27b8b860d7..0000000000 --- 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 0000000000..8009fbd633 --- /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 0000000000..9b540c7aa1 --- /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 0000000000..888e06e021 --- /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 1ebe478576..0000000000 --- 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 0000000000..e2ce5a373d --- /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 0000000000..43af9fe3df --- /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 9de04ae8e6..5a4b9d5616 100644 --- a/tests/components/zha/test_device_automation.py +++ b/tests/components/zha/test_device_automation.py @@ -4,9 +4,6 @@ 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 @@ -14,7 +11,7 @@ from homeassistant.setup import async_setup_component 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 9b8c4ab283..d62dede5ab 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 37f792cec4..63e9956d0d 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -41,7 +41,7 @@ class FoscamCam(Camera): """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 550a0ce1d1..3809249bea 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 56a87ce434..994c3e2fd8 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 07028de0d4..77426e8ae2 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 0f3ae7da71..05170a54ed 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 37fd18efb6..efc0bfbc82 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 d18ff9fc46..60428961cb 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 42d35b60b2..8fa3897b73 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 8b26a958a6..6f1c66d8d8 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 9ec8c56d77..af92f6c5f0 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -373,7 +373,7 @@ class OnkyoDeviceZone(OnkyoDevice): """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 6806df0408..86d26ec25b 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 ef86ea6734..461b3a199d 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 af661f4571..6a64226a05 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 9e62e7ee0d..6540fca140 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 70301f203f..a387b3fc0b 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -81,7 +81,7 @@ class TcpSensor(Entity): 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 60d4957383..e7f87074cb 100644 --- a/homeassistant/components/tplink/device_tracker.py +++ b/homeassistant/components/tplink/device_tracker.py @@ -245,7 +245,7 @@ class Tplink3DeviceScanner(Tplink1DeviceScanner): """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 @@ class Tplink4DeviceScanner(Tplink1DeviceScanner): """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 f14ea5af02..8c83de202a 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 01a3338e54..20aae3849a 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 5af784359d..d0bb27c06e 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -863,7 +863,7 @@ class WinkSirenDevice(WinkDevice): @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 @@ class WinkNimbusDialDevice(WinkDevice): @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 d888fb8252..07d3ff4bec 100644 --- a/homeassistant/components/wink/switch.py +++ b/homeassistant/components/wink/switch.py @@ -53,7 +53,7 @@ class WinkToggleDevice(WinkDevice, ToggleEntity): @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 7c1979f3ab..31cbc0c65b 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 fd18772edb..a28f53f93d 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 e69564453f..dd9e383380 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 ccc55691ee..9822b7c63c 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 ecf904ac9c..74c8a99b51 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 0000000000..6b4e144d9a --- /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 0000000000..cb2fe82129 --- /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 a440fdde4f..3998494606 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 d2610c68ae..171c656566 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 b906d0d8dc..b424be059f 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 0000000000..2a03b7d0e8 --- /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 6a819fbc16..84b335bdea 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 9db55494ba..f639c85c68 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 b9b919c4bf..ae4401178b 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 63ad27a654..c92b16b78f 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 7744b9f223..f46904f326 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 b8d4a15865..3a40af2b1a 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 f46904f326..c4de1d66e8 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 3a40af2b1a..2b9fc63199 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 7f70b0cfac..0606778acb 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -33,7 +33,7 @@ CURRENT_GARAGE_STATE_MAP = { 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 afbb55e03f..53245176a0 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 ecef388c0d..c49319abf4 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 340291807e..26311a4c72 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 26cb3c2094..9aa4544f5c 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 f7e24d6988..b416b1f98d 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 | 67 ++++++++++++++---- tests/components/template/test_switch.py | 75 ++++++++++++++++++++- 2 files changed, 127 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index c77a90c1f8..2d4dda032c 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -19,12 +19,14 @@ from homeassistant.const import ( 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 @@ SWITCH_SCHEMA = vol.Schema( 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() - ) + manual_entity_ids = device_config.get(ATTR_ENTITY_ID) + entity_ids = set() - state_template.hass = hass + 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 = [] - if icon_template is not None: - icon_template.hass = hass + for template_name, template in templates.items(): + if template is not None: + template.hass = hass - if entity_picture_template is not None: - entity_picture_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 @@ class SwitchTemplate(SwitchDevice): state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, entity_ids, @@ -120,9 +153,11 @@ class SwitchTemplate(SwitchDevice): 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 @@ class SwitchTemplate(SwitchDevice): """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 @@ class SwitchTemplate(SwitchDevice): """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 @@ class SwitchTemplate(SwitchDevice): 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 9a07a935d1..3adc5dcad4 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 @@ class TestTemplateSwitch: 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 42d8d89a02..417616161e 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 2b9fc63199..2f8ae35125 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 6e2298e05b..26d0f351fa 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -8,6 +8,7 @@ import voluptuous as vol 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 @@ class XiaomiDevice(Entity): 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 b3f29e93c6..5ad29af0aa 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -19,6 +19,7 @@ SENSOR_TYPES = { "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 26d0f351fa..7a337dcc49 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -324,7 +324,7 @@ class XiaomiDevice(Entity): 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 | 22 +++++++++----- .../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, 59 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 558b0fe420..0ea91d10b1 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -4,8 +4,8 @@ import voluptuous as vol 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) - - old_options = dict(config_entry.options) - - new_options = {CONF_MASTER_GATEWAY: master} - - options = {**old_options, **new_options} + options = {**config_entry.options, CONF_MASTER_GATEWAY: master} hass.config_entries.async_update_entry(config_entry, options=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, data=config) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index c63b172139..488d48bb74 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -5,7 +5,7 @@ import async_timeout 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 @@ from .const import ( 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 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 62879a8272..ad23a56427 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -5,13 +5,15 @@ _LOGGER = logging.getLogger(__package__) 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 fe8f49d260..4aec29008d 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 2f8ae35125..73ececb051 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 c92b16b78f..6f54214736 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 d7071d6dae..4d8d3a3125 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 25a1cd465c..b98681b6fc 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -20,6 +20,7 @@ ENTRY_CONFIG = { 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 7d630498cd..986e01a159 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 1d266d23d3..54abd54caa 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -15,6 +15,7 @@ from homeassistant.components import ( sensor, switch, vacuum, + alarm_control_panel, ) DOMAIN = "google_assistant" @@ -48,6 +49,7 @@ DEFAULT_EXPOSED_DOMAINS = [ "lock", "binary_sensor", "sensor", + "alarm_control_panel", ] PREFIX_TYPES = "action.devices.types." @@ -66,6 +68,7 @@ TYPE_SENSOR = PREFIX_TYPES + "SENSOR" 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_PROTOCOL_ERROR = "protocolError" 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 @@ DOMAIN_TO_GOOGLE_TYPES = { 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 2afa18af32..7d6e79a823 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -16,6 +16,7 @@ from homeassistant.components import ( sensor, switch, vacuum, + alarm_control_panel, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( @@ -31,6 +32,20 @@ from homeassistant.const import ( 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 @@ from .const import ( 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_FANSPEED = PREFIX_TRAITS + "FanSpeed" 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_MODES = PREFIX_COMMANDS + "SetModes" 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 @@ class LockUnlockTrait(_Trait): ) +@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 ccb74e88e3..12de2eaba1 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -230,4 +230,11 @@ DEMO_DEVICES = [ "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 6a7b69daab..6473e8964b 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -7,7 +7,15 @@ from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION 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 0288aa8757..a5c527dacf 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 @@ from homeassistant.components import ( 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 homeassistant.util import color 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 e7706b0435..dd4df479af 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 4eb3881e95..fbf4fffb17 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 @@ class ObihaiServiceSensors(Entity): 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 73ececb051..26fdfc38fa 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 488d48bb74..66df687047 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -90,7 +90,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 4d8d3a3125..d0423c394a 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 a4d6d0d201..a8932f54a5 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 7e05cdf0b3..cb9180d717 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 4e9a4a3a4f..f10e1b4bd6 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 0000000000..9e7e9fed39 --- /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 cb8b7436b5..eb65a7ed42 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." +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} + ) ) - 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.", - ) + return True -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 +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] - if "ecobee" in _CONFIGURING: - configurator = hass.components.configurator - configurator.request_done(_CONFIGURING.pop("ecobee")) + data = EcobeeData(hass, entry, api_key=api_key, refresh_token=refresh_token) - hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP) + if not await data.refresh(): + return False - 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) + await data.update() + + if data.ecobee.thermostats is None: + _LOGGER.error("No ecobee devices found to set up") + return False + + hass.data[DOMAIN] = data + + for component in ECOBEE_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True class EcobeeData: - """Get the latest data and update the states.""" + """ + Handle getting the latest data from ecobee.com so platforms can use it. - def __init__(self, config_file): - """Init the Ecobee data object.""" - from pyecobee import Ecobee + Also handle refreshing tokens and updating config entry with refreshed tokens. + """ - self.ecobee = 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} + ) @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from pyecobee.""" - self.ecobee.update() - _LOGGER.debug("Ecobee data updated successfully") + 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 -def setup(hass, config): - """Set up the Ecobee. +async def async_unload_entry(hass, config_entry): + """Unload the config entry and platforms.""" + hass.data.pop(DOMAIN) - Will automatically load thermostat and sensor components to support - devices discovered on the network. - """ - global NETWORK + tasks = [] + for platform in ECOBEE_PLATFORMS: + tasks.append( + hass.config_entries.async_forward_entry_unload(config_entry, platform) + ) - 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) - - NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) - - setup_ecobee(hass, NETWORK.ecobee, config) - - 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 a3cd49ff45..8b7b819cfc 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 @@ class EcobeeBinarySensor(BinarySensorDevice): @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 181f1561eb..9eb8e8f26b 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 @@ from homeassistant.const import ( ) 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_TO_ECOBEE_HOLD = { 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 @@ SUPPORT_FLAGS = ( ) -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 setup_platform(hass, config, add_entities, discovery_info=None): 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 setup_platform(hass, config, add_entities, discovery_info=None): 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 @@ class Thermostat(ClimateDevice): 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 0000000000..f4cd4fc5bf --- /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 0000000000..c3a23099b8 --- /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 31cca1e676..092594c41f 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 bb6861a149..c7b3f47d29 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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 d21f937dd2..e62d68dc9b 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.const import ( ) 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 @@ SENSOR_TYPES = { } -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 @@ class EcobeeSensor(Entity): @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 @@ class EcobeeSensor(Entity): """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 e69de29bb2..87eefed9ea 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 0000000000..9e7e9fed39 --- /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 b09e06bd82..dd3112b636 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.components.weather import ( ) 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 @@ class EcobeeWeather(WeatherEntity): 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 9a534c01bb..b6865f9e86 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -15,6 +15,7 @@ FLOWS = [ "daikin", "deconz", "dialogflow", + "ecobee", "emulated_roku", "esphome", "geofency", diff --git a/requirements_all.txt b/requirements_all.txt index 26fdfc38fa..5ca7a7c816 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 6f54214736..d6c0d8dbbb 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 649c48e1b7..fcb265bbc9 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -146,6 +146,7 @@ TEST_REQUIREMENTS = ( "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 d6c40ddf9a..90a9a64177 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -54,7 +54,7 @@ class TestEcobee(unittest.TestCase): 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 0000000000..7b4d1f96a3 --- /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 96c749a341..5a3379f71c 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -8,6 +8,9 @@ import random 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 async_setup_entry(hass, config_entry, async_add_entities): tasks.append( async_update_items( hass, + config_entry, bridge, async_add_entities, request_update, @@ -160,6 +164,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): tasks.append( async_update_items( hass, + config_entry, bridge, async_add_entities, request_update, @@ -176,6 +181,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 c0c7c462f9..cb37dd3036 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 19b4a1333b..456678edac 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -157,6 +157,7 @@ class DeviceRegistry: 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 @@ class DeviceRegistry: 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 @@ class DeviceRegistry: 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 5ca7a7c816..6481137f3e 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 d6c0d8dbbb..d42120c339 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 1c891b9c84..582cc185bc 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 b854b62853..1b146e9cb1 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 f534f96b78..dc053e60de 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -156,7 +156,9 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): (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 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): """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 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): 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 bb764e375f..6c02e430ba 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -43,7 +43,9 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): 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 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): 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 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): 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 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): 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 3936aefab0..8f0d090631 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -75,7 +75,9 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light): 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 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light): 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 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light): 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 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light): 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 df42946054..c624aaafa3 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -92,7 +92,9 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchDevice): 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 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchDevice): 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 @@ class MySensorsIRSwitch(MySensorsSwitch): 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 @@ class MySensorsIRSwitch(MySensorsSwitch): 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 0000000000..b0ad4660d0 --- /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 0000000000..9b9741b960 --- /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 0000000000..56822c2365 --- /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 0000000000..0b10e1f51a --- /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 0000000000..139cff2187 --- /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 0000000000..7c82ef8b9b --- /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 f3eead4aae..c9963e4962 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 1a03143f11..840bc8929a 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 0000000000..bd8503fabd --- /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 9e7e9fed39..39072f70d8 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 0000000000..48d6eacda9 --- /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 0000000000..26a55aa4ed --- /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 4fae0249fd..02354204f2 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 533ba76b6a..33b57d9e7c 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 22a9390957..f8f4a2761d 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 0000000000..fb77e5da8c --- /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 58a5169ac0..812de425ef 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 130cf2067a..244044b2f6 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 72f1ad2a4c..e4223e373f 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 efccc652d5..64a3ea94e1 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 199b150f68..31187aaa1b 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 e4aa0d8c08..0de9452b3c 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 0000000000..7fe89d491c --- /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 0000000000..df36ab070d --- /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 d8f571c968..df5654ff78 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 0000000000..e769f83d28 --- /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 0000000000..a12436936e --- /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 0000000000..e75860d0e1 --- /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 642a2c0af1..2715ef46dc 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 3140648f57..8d99b6eebc 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 f3e7053ca1..49a754f1da 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 56c2518f11..5e5c666b1a 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 f639c85c68..36bdfb6d5d 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 cb9180d717..6abb753557 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 f18f5d4f8d..536848d3ae 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 dd4df479af..68045ff058 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 fbf4fffb17..4644875ee8 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 @@ class ObihaiServiceSensors(Entity): """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 @@ class ObihaiServiceSensors(Entity): 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 6481137f3e..5bbd77f4d0 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 dd458dda07..874ac6334a 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 e620e4869e..cf70b7470c 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 requests.exceptions 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 homeassistant.util.json import load_json 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 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """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 4d097253ea..356c7fe574 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -33,12 +33,9 @@ from homeassistant.helpers.event import track_time_interval 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 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 _setup_platform(hass, config_entry, add_entities_callback): 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 _setup_platform(hass, config_entry, add_entities_callback): 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 _setup_platform(hass, config_entry, add_entities_callback): 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 @@ class PlexClient(MediaPlayerDevice): 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 @@ class PlexClient(MediaPlayerDevice): 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 @@ class PlexClient(MediaPlayerDevice): 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 f41a9bdaba..0927447291 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -3,22 +3,29 @@ import plexapi.myplex 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 @@ class PlexServer: 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 c093d4fe0c..812e7b81a7 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 e98aed793c..37cf0fa200 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -28,6 +28,13 @@ MOCK_FILE_CONTENTS = { 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 | 270 ++++++++++++------ .../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 | 25 +- .../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, 723 insertions(+), 128 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 a8932f54a5..d42d7cbb3b 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 6abb753557..11b99b42a4 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 0000000000..7160cd109c --- /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 e7f9b94046..e6ddd87bdf 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 @@ CONFIG_SCHEMA = vol.Schema( 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,70 +56,165 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): - """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] - - import transmissionrpc - from transmissionrpc.error import TransmissionError - - try: - api = transmissionrpc.Client(host, port=port, user=username, password=password) - api.session_stats() - except TransmissionError as error: - if str(error).find("401: Unauthorized"): - _LOGGER.error("Credentials for" " Transmission client are not valid") - return False - - tm_data = hass.data[DATA_TRANSMISSION] = TransmissionData(hass, config, api) - - tm_data.update() - tm_data.init_torrent_list() - - def refresh(event_time): - """Get the latest data from Transmission.""" - tm_data.update() - - track_time_interval(hass, refresh, 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" +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] ) - - hass.services.register( - DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA - ) - - sensorconfig = { - "sensors": config[DOMAIN][CONF_MONITORED_CONDITIONS], - "client_name": config[DOMAIN][CONF_NAME], - } - - discovery.load_platform(hass, "sensor", DOMAIN, sensorconfig, config) - - if config[DOMAIN][TURTLE_MODE]: - discovery.load_platform(hass, "switch", DOMAIN, sensorconfig, config) + ) return True +async def async_setup_entry(hass, config_entry): + """Set up the Transmission Component.""" + 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 + + +async def get_api(hass, host, port, username=None, password=None): + """Get Transmission client.""" + try: + api = await hass.async_add_executor_job( + transmissionrpc.Client, host, port, username, password + ) + return api + + except TransmissionError as error: + 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]} + + hass.config_entries.async_update_entry(config_entry, options=options) + + +class TransmissionClient: + """Transmission Client Object.""" + + 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) + + for platform in ["sensor", "switch"]: + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, platform + ) + ) + + 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) + + return True + + def set_scan_interval(self, scan_interval): + """Update scan interval.""" + + def refresh(event_time): + """Get the latest data from Transmission.""" + self.tm_data.update() + + 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: """Get the latest data and update the states.""" 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 @@ class TransmissionData: 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 @@ class TransmissionData: 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 @@ class TransmissionData: """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 0000000000..99376f4b6e --- /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 0000000000..e4a8b1490c --- /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 0000000000..b5f74f7bf4 --- /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 bc5da64fca..2bd4571ef9 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 ac2e64ce92..30dfa4a3cb 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): - """Set up the Transmission sensors.""" - if discovery_info is None: - return + """Import config from configuration.yaml.""" + pass - transmission_api = hass.data[DATA_TRANSMISSION] - monitored_variables = discovery_info["sensors"] - name = discovery_info["client_name"] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Transmission sensors.""" + + 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 e049f89b3c..ab383584e8 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 0000000000..7160cd109c --- /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 df490cdbe4..0bb43f715a 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 @@ class TransmissionSwitch(ToggleEntity): """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 @@ class TransmissionSwitch(ToggleEntity): 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 b6865f9e86..ab7b339e58 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -62,6 +62,7 @@ FLOWS = [ "tplink", "traccar", "tradfri", + "transmission", "twentemilieu", "twilio", "unifi", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d42120c339..d790a423de 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 fcb265bbc9..1e484e0dfc 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -173,6 +173,7 @@ TEST_REQUIREMENTS = ( "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 0000000000..b8f8d8c847 --- /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 0000000000..e79f5c8ac9 --- /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 | 85 +++++++++++--- .../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, 226 insertions(+), 93 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 11b99b42a4..419bc1a860 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 1d8ed8e37b..0b51be1f25 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) + return success + + +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) - - _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") + 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 0000000000..51c5f7a9db --- /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 584ea22afe..b911aaa57e 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 2d4549a21d..501566de68 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 @@ class RainBirdSensor(Entity): 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 0000000000..cdac7171a2 --- /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 868e8ff4c7..cb4ac83090 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 @@ class RainBirdSwitch(SwitchDevice): 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 5bbd77f4d0..3b9eb71873 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 9eb8e8f26b..460bd2bb4a 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -33,12 +33,21 @@ from homeassistant.const import ( 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_TO_ECOBEE_HOLD = { 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 @@ RESUME_PROGRAM_SCHEMA = vol.Schema( } ) +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 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 @@ class Thermostat(ClimateDevice): # 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 092594c41f..131c35d7f8 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 87eefed9ea..2155d3cf7d 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 3b9eb71873..19bdc4efd8 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 d790a423de..3a4fa60f15 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 | 52 +++++------ .../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, 653 insertions(+), 156 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 0f854db0be..813405cec6 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 94535d7e94..2cd6cd72f6 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 5c8d75d4cc..f1ca796363 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 054b1a89ae..f446bf7390 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 be337bd3f0..3b21726bcb 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 a6ce441749..88be279ae7 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 7ba95b007c..675c5e1877 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 a4300e1e53..5a816bce14 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 cae2e940d5..c5d84f030f 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 28859cbf59..1539429d0e 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 7e3ed3ef88..6c7c88a804 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 dd1d6f5437..5603c14fe1 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 1457db2b60..c0d0df0213 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 0000000000..36c72dcb9e --- /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 d36c929e72..6b8166f23c 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 d5383fb1a2..711ac32039 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 1699bcad8f..457b7d1b89 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 0f4bdf98ac..42b14833aa 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 1bc6c8211a..cb5db0b834 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 3968c1f00c..c7079fd621 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 f024386aa0..bd47a63776 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 21da9b4823..db41ee98e0 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 e9eabeff63..0be75b94be 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 e27d59a40e..4d23ac8aab 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 18a476b687..b6bd30f799 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 18d3d92e16..3cb54145ad 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 0000000000..7a42a9470d --- /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 0000000000..2bf141f648 --- /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 0000000000..660e0064bb --- /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 0000000000..f4a63bb449 --- /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 0000000000..e1eb6ebd35 --- /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 70d766cf4c..be8033f316 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 4409616cef..1956c453a9 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 bec33c26d1..003a3db8bf 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 836b5c8bc6..488a09c376 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 c9d98da1af..c21e76a6ee 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 dd4cb48a09..d41051b667 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 c45f9c467e..9efacaf163 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 7340569e64..68e87e9aea 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 f95ccf5727..c6a960f1a6 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 7a08b44f25..6bbe75a801 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 481ab372e9..2fe38659fa 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 a125b96a07..798ae93d33 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 fcd5c1df01..9068b18c82 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 63080a30ff..ae32d43f1b 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 4c66f0d013..908394bcfd 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 c9b727088a..b8b3bbb812 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 4ea4a94014..14a747f6ef 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 2fe1c6b42d..e07adeb0a3 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 0000000000..7d7e158f3c --- /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 63954ca83a..e742a337cc 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 f8f4a2761d..17c81c471f 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 de62d2ac2f..4fdef0a987 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 8e9b3272f9..c5ad304300 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 62bb6bb9d9..7dd598c8fc 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 c68f49509b..23b951bfa6 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 b9f82b6e2e..1eb3b71c94 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 91c616b69a..3d1cbd41a3 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 4b9ab3a7ab..f16533312f 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 b3f1e4740b..99a760dce4 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 03cf1a82b8..743c47e00c 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 af89dd3d39..f672f519f4 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 9f86cd12cc..5838dcad30 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 d8c195cb27..923f452450 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 4b47f52eef..0de31a35eb 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 20cdb405f4..1bf211476d 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 ea680a638e..670bc23ca1 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 7fa9f62be0..d3c4c0d5a7 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 b58cdfe728..393639dd4c 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 b424be059f..c547e4306b 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 0000000000..702cec128c --- /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 c79a49470e..2cf3fa2c1a 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 9f688b2e5f..f1bbb1c872 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 6d15210811..2053d2f4a8 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 f0a71b4be5..a786b0c74d 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 c38afd989d..7f80900d82 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 6b53e94330..f7f7f4a1d8 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 c837abad49..ae47916ac8 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 520a29b760..c6fb13c360 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 4946eb81f0..41148475a5 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 d44faf195e..250a5509c4 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 c835ddf76b..dccd39b699 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 dea146b649..805b952690 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 f540245429..85e8994dc5 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 0000000000..3a619d8154 --- /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 7160cd109c..e1bc8dc322 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -1,39 +1,39 @@ { "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." - } + }, + "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": { + "data": { + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port", + "username": "User name" + }, + "title": "Setup Transmission Client" + } + }, + "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 0000000000..f6ddce2a4a --- /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 0000000000..5da2d4f9ef --- /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 0000000000..30004af17d --- /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 0d28b09434..c3d6ff16e2 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 2e85ef7b2d..858970539c 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 2d5bd9027a..498afcbb10 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 813509121e..fb1508a1aa 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 2a036a1d2f..1611dac272 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 917eb0ef3a..25a4172f00 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 71934516ea..7f32ade8a0 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 9e408eb0d5..f01109b2d6 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 36bdfb6d5d..623c33637e 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 818d041b4f..2762adc0fb 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 e31e42bfbc..bbfb3fe712 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 f70eaa4826..1d5584a82a 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 6643faa85b..c085addad4 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 d68f47b1b0..fcf4950f5e 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -3,6 +3,12 @@ import functools 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_decorator(override_available=False): "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 @@ class ADBDevice(MediaPlayerDevice): # 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 19bdc4efd8..a9ec02ffcc 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 3a4fa60f15..c3f40e1634 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 86d1c1c15b..73aa522598 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -4,34 +4,24 @@ from socket import error as socket_error 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 @@ class DeviceFake: 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 patch_shell(response=None, error=False): 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 39b392c97e..feffc70d84 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 7587d0cd42..db79d81736 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 @@ SENSOR_TYPES = { "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 @@ SENSOR_TYPES = { "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 @@ SENSOR_TYPES = { ], }, "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 419bc1a860..4a6dfdbf6e 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 0d80ce770c..5baf928352 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 d7ca22b2be..e837cc4bbb 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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] - if devices: - add_entities(devices, True) + 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) + + +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 @@ class XboxSensor(Entity): 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 @@ class XboxSensor(Entity): """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 b7ff9d17fe..42c16919a4 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 2b3f9f34ad..c05eed2a89 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 edc51dcc53..b89947343c 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 a9ec02ffcc..3298306377 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 dc4b0bd33a..eec7108cde 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -192,7 +192,10 @@ class NestThermostat(ClimateDevice): 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 @@ class NestThermostat(ClimateDevice): if self._mode == NEST_MODE_ECO: return PRESET_ECO - return None + return PRESET_NONE @property def preset_modes(self): @@ -294,7 +297,7 @@ class NestThermostat(ClimateDevice): 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 29e68a5d7a..31908166dd 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 31908166dd..626c056cef 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 f4cd4fc5bf..56ce13f770 100644 --- a/homeassistant/components/ecobee/config_flow.py +++ b/homeassistant/components/ecobee/config_flow.py @@ -33,7 +33,11 @@ class EcobeeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 626c056cef..29e68a5d7a 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 4dfe04f9c2..70699de394 100644 --- a/homeassistant/components/kaiterra/air_quality.py +++ b/homeassistant/components/kaiterra/air_quality.py @@ -82,6 +82,11 @@ class KaiterraAirQuality(AirQualityEntity): """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 0c2d6d9366..81e28438d5 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -23,7 +23,7 @@ from .const import ( _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 | 19 ++++-- 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, 210 insertions(+), 31 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 0000000000..04b764e271 --- /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 fe2d65edef..eb3e5a95c9 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 6d4b465fce..569d1de6a0 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -5,10 +5,11 @@ import os import voluptuous as vol -from homeassistant.core import callback -from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID -from homeassistant.setup import ATTR_COMPONENT 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.util.yaml import load_yaml, dump DOMAIN = "config" @@ -80,6 +81,7 @@ class BaseEditConfigView(HomeAssistantView): 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 @@ class BaseEditConfigView(HomeAssistantView): 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 @@ class BaseEditConfigView(HomeAssistantView): 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 17efdba3fb..97ddf1e071 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -3,6 +3,7 @@ from collections import OrderedDict 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 async_setup(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 b444abd523..62d338ece5 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -9,6 +9,8 @@ from homeassistant.components import websocket_api 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 d3bd97dad8..0e840e1d00 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 113f2437ce..d567962e32 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -90,7 +90,7 @@ def has_at_least_one_key(*keys: str) -> Callable: 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 1a9a3d256a..272271eefb 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 b05c04a16f..6dcd8391bf 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 9e5ea15293..5228f0d488 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 bd4f37bd13..18143c088b 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 29e68a5d7a..51c5cdb936 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 84b335bdea..ea1ad48bbf 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 37bc6c7a2c..b07658e72d 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -10,7 +10,14 @@ import logging 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 0000000000..27e78507bf --- /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 46e3beafca..37ec14bc43 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -9,8 +9,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF 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 cfc32a020c..a41f6de24b 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 0000000000..6e7bc6ab4b --- /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 5a4b9d5616..2f4ddb6b8b 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 dbec148450..e225407329 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -277,6 +277,7 @@ DISCOVERY_SCHEMAS = [ 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 240809548e..0820feb8d0 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 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 f18a66d9a5..cec93f5af4 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 03d153f592..f0d72af23d 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -52,6 +52,8 @@ from .capabilities import ( ENTITY_ADAPTERS = Registry() +TRANSLATION_TABLE = dict.fromkeys(map(ord, r"}{\/|\"()[]+~!><*%"), None) + class DisplayCategory: """Possible display categories for Discovery response. @@ -134,15 +136,18 @@ class AlexaEntity: 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 bd5a4d25ed..3cafa89902 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 37ec14bc43..331dc3d329 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 22cdee8f69..2258840f43 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 801b8ebb5f..47cb9709c3 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -29,5 +29,32 @@ Reproduce state code has been added to the {info.domain} integration: - {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 0000000000..d5674f01b2 --- /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 0000000000..f8a00bf1ec --- /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 0000000000..d19fa8817a --- /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 0000000000..d9cef08351 --- /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 0000000000..f7e9fc091f --- /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 0000000000..c22197bb13 --- /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 01c5c12efb..b14f5fedc2 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -251,8 +251,10 @@ class NotifyAuthModule(MultiFactorAuthModule): _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 4e417fca21..9829044a53 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -215,8 +215,9 @@ class TotpSetupFlow(SetupFlow): 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 952ca473fd..e6a70c9f64 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -42,8 +42,10 @@ class CachingStaticResource(StaticResource): 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 31761f2560..e011db33c3 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 d567962e32..d0aeb4f496 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 db1c34d8fd..881534b5be 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 271b9caa62..d43658d158 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) 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 7c61a8ab1e..f81c40a52b 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -6,7 +6,7 @@ detect_location_info and elevation are mocked by default during tests. 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 236e2fc1aa..79cb2607b1 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 @@ def catch_log_coro_exception( 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 ae4401178b..7e5be09a28 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 c3f40e1634..a860c67dbd 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 47cb9709c3..ab87799d6b 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -28,7 +28,8 @@ 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. +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 @@ Please update the relevant items marked as TODO before submitting a pull request 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 @@ Device trigger base has been added to the {info.domain} integration: 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 @@ Device condition base has been added to the {info.domain} integration: 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 fc96f2d8c9..c11eb18acc 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 @@ CONFIG_SCHEMA = vol.Schema( 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 50c8277302..ce7e53b77a 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)]) + 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 @@ class HiveBinarySensorEntity(BinarySensorDevice): """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 861957e6ef..1fb77ce6cb 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -5,13 +5,14 @@ from homeassistant.components.climate.const import ( 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 @@ class HiveClimateEntity(ClimateDevice): """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 @@ class HiveClimateEntity(ClimateDevice): 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 @@ class HiveClimateEntity(ClimateDevice): """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 a85c3a4399..41fc286d13 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -10,32 +10,28 @@ from homeassistant.components.light import ( ) 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)]) + 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 @@ class HiveDeviceLight(Light): """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 @@ class HiveDeviceLight(Light): """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 @@ class HiveDeviceLight(Light): 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 886d6841eb..2e7c4f4f17 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 c43fe461a8..ccd635015d 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)]) + 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 @@ class HiveSensorEntity(Entity): """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 @@ class HiveSensorEntity(Entity): 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 0000000000..27d7acfc83 --- /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 75efdfe3e5..1447f5483a 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)]) + 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 @@ class HiveDevicePlug(SwitchDevice): """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 @@ class HiveDevicePlug(SwitchDevice): """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 1b009582c1..c60a9ec01d 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) - - add_entities([water_heater]) + devs = [] + for dev in discovery_info: + devs.append(HiveWaterHeater(session, dev)) + add_entities(devs) -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 @@ class HiveWaterHeater(WaterHeaterDevice): """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 @@ class HiveWaterHeater(WaterHeaterDevice): @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 @@ class HiveWaterHeater(WaterHeaterDevice): """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 3298306377..a68f7f71e9 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 525a4c8e72..0e9e030128 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 d43658d158..6920e0d97f 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 @@ except AttributeError: 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 59f6e4d1c6..621950965f 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -102,6 +102,11 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): 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 0000000000..02443d449c --- /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 139cff2187..7862644c72 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 0000000000..1982dd4084 --- /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 0000000000..5c51d86fee --- /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 244044b2f6..1e6488784d 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 393639dd4c..f7a6bfd9c7 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 606f97d696..ea1db4ec2f 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 0000000000..6cc611fcb7 --- /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 0000000000..9b45e31076 --- /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 8d99b6eebc..0b800ecd80 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 ea1ad48bbf..d8e8955a93 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 3a95b90db9..6f10344986 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 a68f7f71e9..bd0a11f373 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 460bd2bb4a..6eccdccf8c 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -37,6 +37,7 @@ from homeassistant.util.temperature import convert 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 @@ DTGROUP_INCLUSIVE_MSG = ( 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 @@ CREATE_VACATION_SCHEMA = vol.Schema( 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 0000000000..3acc3e5676 --- /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 0000000000..ee02f2a33a --- /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 0000000000..473cdfff1e --- /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 103a9fce31..c7b5535d03 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 28c02080f1..1c1eed0e89 100644 --- a/homeassistant/components/yessssms/notify.py +++ b/homeassistant/components/yessssms/notify.py @@ -3,11 +3,16 @@ import logging 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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 @@ class YesssSMSNotificationService(BaseNotificationService): 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 bd0a11f373..53f72801cc 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 a860c67dbd..7ac46e96cd 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 3d11cdedc6..5cc204ccc4 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 @@ class TestNotifyYesssSMS(unittest.TestCase): 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 @@ class TestNotifyYesssSMS(unittest.TestCase): "POST", # pylint: disable=protected-access self.yessssms.yesss._login_url, - exc=ConnectionError, + exc=yessssms.YesssSMS.ConnectionError, ) message = "Testing YesssSMS platform :)" @@ -209,4 +351,4 @@ class TestNotifyYesssSMS(unittest.TestCase): 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 4e253741b0..71e1a81135 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 53f72801cc..ab99704f96 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 51b9a523b3..483ee1ae87 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -38,6 +38,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 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 @@ COVER_SCHEMA = vol.Schema( 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 @@ class CoverTemplate(CoverDevice): tilt_template, icon_template, entity_picture_template, + availability_template, open_action, close_action, stop_action, @@ -213,6 +223,7 @@ class CoverTemplate(CoverDevice): 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 @@ class CoverTemplate(CoverDevice): 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 @@ class CoverTemplate(CoverDevice): 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 @@ class CoverTemplate(CoverDevice): """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 @@ class CoverTemplate(CoverDevice): ) 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 @@ class CoverTemplate(CoverDevice): ) 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 247ee25027..d3be01cbdc 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -16,7 +16,10 @@ from homeassistant.const import ( 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 e0fc867720..d5ade703c9 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -26,6 +26,7 @@ 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, async_track_same_state +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -38,6 +39,7 @@ SENSOR_SCHEMA = vol.Schema( 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 @@ class BinarySensorTemplate(BinarySensorDevice): value_template, icon_template, entity_picture_template, + availability_template, entity_ids, delay_on, delay_off, @@ -156,12 +162,14 @@ class BinarySensorTemplate(BinarySensorDevice): 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 @@ class BinarySensorTemplate(BinarySensorDevice): """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 @@ class BinarySensorTemplate(BinarySensorDevice): 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 @@ class BinarySensorTemplate(BinarySensorDevice): _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 c8cec168d6..143811da20 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -3,7 +3,13 @@ from datetime import timedelta 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 @@ class TestBinarySensorTemplate(unittest.TestCase): template_hlpr.Template("{{ 1 > 1 }}", self.hass), None, None, + None, MATCH_ALL, None, None, @@ -298,6 +305,7 @@ class TestBinarySensorTemplate(unittest.TestCase): 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 7fd8c4d9b3..42790e618d 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -33,6 +33,7 @@ from homeassistant.core import callback 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 @@ FAN_SCHEMA = vol.Schema( 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 @@ class TemplateFan(FanEntity): speed_template, oscillating_template, direction_template, + availability_template, on_action, off_action, set_speed_action, @@ -175,6 +181,8 @@ class TemplateFan(FanEntity): 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 @@ class TemplateFan(FanEntity): 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 @@ class TemplateFan(FanEntity): """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 @@ class TemplateFan(FanEntity): ", ".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 b80522b37e..5753684795 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -5,7 +5,7 @@ import pytest 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 @@ _LOGGER = logging.getLogger(__name__) _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 320dcd2e22..552c21f170 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -28,6 +28,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 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 @@ LIGHT_SCHEMA = vol.Schema( 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 @@ class LightTemplate(Light): state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, level_action, @@ -147,6 +157,7 @@ class LightTemplate(Light): 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 @@ class LightTemplate(Light): 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 @@ class LightTemplate(Light): 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 @@ class LightTemplate(Light): """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 @@ class LightTemplate(Light): @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 @@ class LightTemplate(Light): 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 87fd8cd4db..c2dd49a76f 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -4,13 +4,16 @@ import logging 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 @@ class TestTemplateLight: 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 5374247dac..6a6523514c 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -44,6 +44,8 @@ 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__) CONF_VACUUMS = "vacuums" @@ -67,6 +69,7 @@ VACUUM_SCHEMA = vol.Schema( 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 @@ class TemplateVacuum(StateVacuumDevice): state_template, battery_level_template, fan_speed_template, + availability_template, start_action, pause_action, stop_action, @@ -198,6 +205,7 @@ class TemplateVacuum(StateVacuumDevice): 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 @@ class TemplateVacuum(StateVacuumDevice): 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 @@ class TemplateVacuum(StateVacuumDevice): """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 @@ class TemplateVacuum(StateVacuumDevice): 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 9e3c535f13..da0e8e59ed 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -3,7 +3,7 @@ import logging 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 05853d4b26..1273278189 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -6,6 +6,8 @@ from homematicip.aio.device import AsyncDevice 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 @@ class HomematicipGenericDevice(Entity): 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 @@ class HomematicipGenericDevice(Entity): 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 @@ class HomematicipGenericDevice(Entity): 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 23973efb07..abba183d33 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -5,6 +5,7 @@ import logging 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 @@ class HomematicipHAP: 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 @@ class HomematicipHAP: 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 b83358822b..2075f88ded 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 ab99704f96..9b814ef8ed 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 7ac46e96cd..9599d24b20 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 0000000000..34b601027c --- /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 b0c8051e69..41dd3c00d2 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 0000000000..e53d918f98 --- /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 0000000000..0583a4d4f7 --- /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 7862644c72..059800a116 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 5bf8db4684..9e81091074 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 70c33cf3c0..d92f318f61 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 0000000000..2ecb587f19 --- /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 17c81c471f..4b649744ed 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 3b34d8f635..dcbc40b4c7 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 2e77b4ba97..3c28f1d25f 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 ea1db4ec2f..ce9d2e1e88 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 c4d616600f..ad8a15d06b 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 0000000000..c3ea319069 --- /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 31187aaa1b..201a77a76a 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 0000000000..17a03b6dba --- /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 e4b87c9d7b..bb05977fd0 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 93867c0c84..76f1c58fe7 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 7e1ae1ecd6..7be31d56c0 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -11,14 +11,20 @@ from homeassistant.components.climate.const import ( 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 @@ class VenstarThermostat(ClimateDevice): @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 @@ class VenstarThermostat(ClimateDevice): 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 c11eb18acc..3301097bab 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) 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_HEATING_SCHEMA = vol.Schema( } ) -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 setup(hass, config): 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 setup(hass, config): 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 2e7c4f4f17..d9fae3fe54 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 27d7acfc83..6513d76ca8 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 9b814ef8ed..79313b48e6 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 7da2fcee38..31b895b4eb 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -91,11 +91,11 @@ class EsphomeCover(EsphomeEntity, CoverDevice): 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 184b9ea302..154394075a 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 @@ TRIGGER_SCHEMA = vol.All( 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 62d338ece5..23e320fe15 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 homeassistant.loader import async_get_integration, IntegrationNotFound 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 b7cadd1349..ef1b605f4d 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 import condition, config_validation as cv, service 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 8ef662ec87..e46423c827 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -4,7 +4,7 @@ import logging 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 75b4547198..204fcab038 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.entity_component import EntityComponent 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 faa4ddfc87..c5200082f2 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 @@ from homeassistant.const import ( 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 @@ from homeassistant.components.cover import ( CoverDevice, ) + +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) KEY_OPEN_CLOSE = "open_close" @@ -76,13 +80,25 @@ class CoverGroup(CoverDevice): 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 0b1291d404..e77c858fc0 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -3,7 +3,7 @@ import asyncio 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 @@ from homeassistant.components.light import ( 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 @@ class LightGroup(light.Light): 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 3d3c644fea..2ffb7fea04 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -17,6 +17,9 @@ from homeassistant.components.notify import ( 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 791dacb702..98da19fd98 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -7,6 +7,7 @@ import functools as ft 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 @@ class MediaPlayerDevice(Entity): 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 6c49784ede..6b9c7c44dd 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.loader import bind_hass 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 @@ async def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: @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 3e6048e153..7d883e273e 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -17,6 +17,9 @@ from homeassistant.helpers.sun import ( ) 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 57ab34a6a5..1ec758ebd4 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -4,6 +4,9 @@ from homeassistant.loader import bind_hass 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 2d748f5cc6..716b20f4ca 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 homeassistant.const import __version__ 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 @@ class AuthPhase: 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 deb3600574..9d46238b24 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -12,6 +12,9 @@ from homeassistant.helpers.event import async_track_state_change 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 3886d6c21d..41232b097d 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 homeassistant.exceptions import Unauthorized from . import const, messages +# mypy: allow-untyped-calls, allow-untyped-defs + + class ActiveConnection: """Handle an active websocket client connection.""" @@ -22,7 +27,7 @@ class ActiveConnection: 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 ba65d5e19a..025131643e 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -8,6 +8,8 @@ from homeassistant.exceptions import Unauthorized 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 9a1f375fdf..17a6709496 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -25,6 +25,9 @@ from .error import Disconnect 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 @@ class WebSocketHandler: 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 @@ class WebSocketHandler: # 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 @@ class WebSocketHandler: 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 @@ class WebSocketHandler: 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 65291bc55e..c8c760a654 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -7,6 +7,8 @@ from homeassistant.helpers import config_validation as cv 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 1ae76b5625..20a6a90860 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -10,6 +10,9 @@ from .const import ( ) +# 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 2ee03c0818..6ae62be3eb 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 .config_flow import configured_zones 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 05ba28e4ca..d23fb5a475 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 @@ from homeassistant.const import ( 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 ccd8e55a4c..f084492bd3 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 @@ from .const import ATTR_PASSIVE, ATTR_RADIUS 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 e011db33c3..f4be3b6632 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -28,6 +28,7 @@ from typing import ( Set, TYPE_CHECKING, Awaitable, + Mapping, ) from async_timeout import timeout @@ -704,7 +705,7 @@ class State: 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 3b12864621..0bc27498f7 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 @@ class FlowHandler: 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 @@ class FlowHandler: 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 @@ class FlowHandler: @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 @@ class FlowHandler: @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 @@ class FlowHandler: } @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 fad02dee07..836ad954ae 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 @@ from homeassistant.const import ( 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 1fa0ec76a6..dc48d82534 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 f3866f40e5..08413ecd23 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 7e5be09a28..9da375b33c 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 9599d24b20..2701513a6d 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 d4b0075aa6..d4222f1d2e 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 0000000000..b0ad4660d0 --- /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 434c236418..de7d837b12 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 0000000000..8e2d326d9d --- /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 059800a116..a7f0bd516a 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 0000000000..d26f0f9470 --- /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 d92f318f61..11a1beb10d 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 1d6a849f3a..c106543e15 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 0000000000..2c4d16b578 --- /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 0000000000..5544d2e7f7 --- /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 0000000000..d70be59afb --- /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 0000000000..9f82b1a7b1 --- /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 4b649744ed..33a38fc930 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 4c24dddbe8..1460786890 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 0000000000..6d1ad1f62d --- /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 2a03b7d0e8..49ed34baf7 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 2cf3fa2c1a..5f6d0c41c1 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 201a77a76a..09b43f4100 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 0000000000..9fc23b3e39 --- /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 0000000000..395f6e2d68 --- /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 0000000000..f210eb42f8 --- /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 0000000000..a2d3d177e2 --- /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 0000000000..122c332f42 --- /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 0000000000..479e25c6d8 --- /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 a7eac61bab..ea13035e09 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 fac325a709..ee0cf52358 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 635d0ecbde..2b8230ad68 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 0047c762a9..b8529ce904 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 76f1c58fe7..0e1b7028db 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 8606a04e19..0bc3afe28e 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 30df6716f9..226bd37200 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 bbfb3fe712..d7f421c7e8 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 c4de1d66e8..f0f6389a06 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 79313b48e6..dadf60e166 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 ffc9827eed..00bb0e2d67 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -5,7 +5,13 @@ import voluptuous as vol 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 @@ from .const import ( 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 @@ CONFIG_SCHEMA = vol.Schema( { 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 @@ CONFIG_SCHEMA = vol.Schema( 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 ba83bf1d80..5422054795 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -12,6 +12,10 @@ DEFAULT_NAME = "Pi-Hole" 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 0000000000..b16ed21a5d --- /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 e95381cdf7..5296f34662 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 @@ from homeassistant.const import ( ) 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 @@ class DarkSkyWeather(WeatherEntity): 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 @@ class DarkSkyWeather(WeatherEntity): 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 038787ac8d..1607000e8d 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -94,13 +94,11 @@ async def async_setup(hass, config): 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_setup(hass, config): _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 | 34 ++++--- 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, 125 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 004086ab5c..39a45429cb 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 @@ from . import DOMAIN 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._client = client - self._boiler = boiler + self._unique_id = f"{heater.serial_no}_failed" - async def async_added_to_hass(self): + self._client = client + self._heater = heater + + 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 cccb9d2564..3918244d4e 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 homeassistant.helpers.dispatcher import async_dispatcher_connect 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 @@ class InComfortClimate(ClimateDevice): 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 @@ class InComfortClimate(ClimateDevice): """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 @@ class InComfortClimate(ClimateDevice): @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 8b5f461c8a..c26ba27a29 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 e19bbf42ae..772b5dab18 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._name = None + self._unique_id = f"{heater.serial_no}_{slugify(name)}" + + 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 @@ class IncomfortSensor(Entity): 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 2449a1223c..7042361170 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 @@ from . import DOMAIN _LOGGER = logging.getLogger(__name__) -HEATER_SUPPORT_FLAGS = 0 - -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", -] +HEATER_ATTRS = ["display_code", "display_text", "is_burning"] -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 @@ class IncomfortWaterHeater(WaterHeaterDevice): 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 @@ class IncomfortWaterHeater(WaterHeaterDevice): 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 dadf60e166..c25ca6a54f 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 e78a8e6851..afb273331a 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 d42d7cbb3b..f28e9aaeda 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 4a6dfdbf6e..db0ff3226c 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 0000000000..738d0fd642 --- /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 0000000000..5bf51e743e --- /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 0000000000..e2f8927352 --- /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 0000000000..815a0176e7 --- /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 0000000000..1577b7f291 --- /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 0000000000..35a77c063b --- /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 0000000000..eac817ce11 --- /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 ab7b339e58..21f57934e9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -55,6 +55,7 @@ FLOWS = [ "smartthings", "smhi", "solaredge", + "soma", "somfy", "sonos", "tellduslive", diff --git a/requirements_all.txt b/requirements_all.txt index c25ca6a54f..5482af01cc 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 2701513a6d..801c09f322 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 1e484e0dfc..9991a6bc1f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -143,6 +143,7 @@ TEST_REQUIREMENTS = ( "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 0000000000..8d84668e5e --- /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 0000000000..764a18d1b8 --- /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 51c5cdb936..60eff86667 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 131c35d7f8..148e355a3d 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 5482af01cc..f6f78bab91 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 801c09f322..faf775ac5b 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 00bb0e2d67..95351083b5 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 be7f0a524d..fa704a6550 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 f6f78bab91..54a9ec2d43 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 d26f0f9470..06534314e0 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 d066cbcc51..8d54c47084 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 1982dd4084..ee1fd5246c 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 0000000000..65394faba1 --- /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 0000000000..7e3ee3b24d --- /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 ca4cee896b..5181c37969 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 0000000000..05414b1e03 --- /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 0000000000..23f519353b --- /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 0000000000..9a759e309c --- /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 0000000000..6bd4737d6f --- /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 0000000000..460f01e301 --- /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 738d0fd642..5dea73fcc2 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 0000000000..ce8e950dac --- /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 0000000000..5ab3af0ecf --- /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 0000000000..7dd523f366 --- /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 9fc23b3e39..4fa0c4e671 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 a2d3d177e2..cabbb6d914 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 0000000000..73735dda1d --- /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 ea13035e09..113eaa000f 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 0000000000..cb2031dc7e --- /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 49a754f1da..a289e05e66 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 623c33637e..95550ca099 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 0bc3afe28e..7ccc661dd2 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 4644875ee8..89bfee7d4e 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -111,6 +111,25 @@ class ObihaiServiceSensors(Entity): 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 c84a415258..77f3c64752 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 54a9ec2d43..561e334541 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 faf775ac5b..e7e4ed37e0 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 b3982e7327..ad04b8a0eb 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 @@ DEVICE_ATTRIBUTES = [ "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 969c2a734d..760e1e4fa4 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 ba7a72024e..14bf122395 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -4,6 +4,7 @@ Such systems include evohome (multi-zone), and Round Thermostat (single zone). """ from datetime import datetime, timedelta import logging +import re from typing import Any, Dict, Optional, Tuple import aiohttp.client_exceptions @@ -25,9 +26,9 @@ import homeassistant.helpers.config_validation as cv 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 @@ CONFIG_SCHEMA = vol.Schema( ) -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 @@ class EvoBroker: # 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 @@ class EvoBroker: 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 @@ class EvoBroker: 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 @@ class EvoBroker: ) 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 @@ class EvoBroker: _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 @@ class EvoBroker: 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 @@ class EvoDevice(Entity): 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 @@ class EvoDevice(Entity): @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) + 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") - if "setpoints" in self._state_attributes: - status["setpoints"] = self.setpoints - - return {"status": status} + return {"status": convert_dict(status)} @property def icon(self) -> str: @@ -388,27 +366,98 @@ class EvoDevice(Entity): """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()) + + 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._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 state data.""" - if ( - not self._schedule.get("DailySchedules") - or parse_datetime(self.setpoints["next"]["from"]) < utcnow() - ): - try: - self._schedule = await self._evo_device.schedule() - except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - _handle_exception(err) + """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 0264f76f38..e5c8c6af14 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.const import PRECISION_TENTHS 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 @@ EVO_PRESET_TO_HA = { } 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 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): 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 @@ class EvoZone(EvoClimateDevice): 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 @@ class EvoZone(EvoClimateDevice): @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 @@ class EvoZone(EvoClimateDevice): @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 @@ class EvoZone(EvoClimateDevice): 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 @@ class EvoController(EvoClimateDevice): """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 @@ class EvoThermostat(EvoZone): 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 a0480d62a1..444671cf82 100644 --- a/homeassistant/components/evohome/const.py +++ b/homeassistant/components/evohome/const.py @@ -21,5 +21,3 @@ EVO_PERMOVER = "PermanentOverride" # 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 1b37bc3b2b..b65665eb2c 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -3,27 +3,31 @@ import logging 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] + """Set new operation mode for a DHW controller. - state = "" if op_mode == EVO_FOLLOW else HA_STATE_TO_EVO[STATE_OFF] - until = None # EVO_FOLLOW, EVO_PERMOVER - - 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"))) - data = {"Mode": op_mode, "State": state, "UntilTime": until} + 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)) - await self._call_client_api( - self._evo_device._set_dhw(data) # pylint: disable=protected-access - ) + 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()) + + async def async_update(self) -> None: + """Get the latest state data for a DHW controller.""" + await super().async_update() + + 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 f28e9aaeda..aa8f2d8c03 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 db0ff3226c..f5cd03882c 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 0000000000..03277bba4d --- /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 0000000000..c0367c4790 --- /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 0000000000..fa06b2b912 --- /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 561e334541..ef1b56222b 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 d7f501987f..aa8cc8b122 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -19,6 +19,7 @@ from homeassistant.const import ( 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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 @@ class TemplateLock(LockDevice): hass, name, value_template, + availability_template, entity_ids, command_lock, command_unlock, @@ -89,10 +103,12 @@ class TemplateLock(LockDevice): 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 @@ class TemplateLock(LockDevice): """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 @@ class TemplateLock(LockDevice): 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 24cde24051..d1d3020737 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -5,7 +5,7 @@ from homeassistant.core import callback 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 @@ class TestTemplateLock: 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 @@ class TestTemplateLock: 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 8cba3032f5..29eecdfd07 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.components.device_tracker.const import ( ) 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 setup_scanner(hass, config, see, discovery_info=None): # 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 307142ed98..01d2fb948e 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -26,7 +26,6 @@ from homeassistant.components.camera import ( ) 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 @@ class GenericCamera(Camera): 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 204fcab038..39574a2b03 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -34,7 +34,6 @@ 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 @@ -430,7 +429,7 @@ class Group(Entity): mode=None, ): """Initialize a group.""" - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( Group.async_create_group( hass, name, @@ -546,7 +545,7 @@ class Group(Entity): 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 8d83cd0cc2..9b25a6ef6e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -39,7 +39,7 @@ from homeassistant.helpers import config_validation as cv, template 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 53a4f620dc..b1ce8ad7ac 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -9,7 +9,6 @@ from homeassistant.components.camera import PLATFORM_SCHEMA, Camera 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 @@ class ProxyCamera(Camera): 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 f4be3b6632..feb4445d36 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -64,11 +64,7 @@ from homeassistant.exceptions import ( 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 @@ class HomeAssistant: 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 @@ class ServiceRegistry: 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 7d5debd484..5c59dc6c13 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -6,7 +6,7 @@ from typing import Optional 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 @@ class EntityPlatform: "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 14ff873d4d..a4f6afa163 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.event import ( 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 @@ class Script: 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 f29d1885d1..e177c86c65 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -20,7 +20,6 @@ from homeassistant.loader import async_get_integration, bind_hass 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 7f9692b338..2f49a566a3 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -44,7 +44,6 @@ from homeassistant.const import ( 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 58e4fc19eb..07de3b2942 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -10,7 +10,6 @@ from homeassistant import requirements, core, loader, config as conf_util 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 @@ SLOW_SETUP_WARNING = 10 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 6920e0d97f..64bedfe250 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -7,7 +7,7 @@ from asyncio.events import AbstractEventLoop 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 @@ except AttributeError: 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 79cb2607b1..99e606d286 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -8,8 +8,6 @@ import threading 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 @@ class AsyncHandler: 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 bc39b1f5e0..1982e80dfe 100644 --- a/tests/common.py +++ b/tests/common.py @@ -53,7 +53,7 @@ from homeassistant.helpers import ( 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 get_test_home_assistant(): 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 09793b4303..17bcaadb92 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -17,7 +17,6 @@ from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM 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 @@ class TestGetImage: """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 @@ class TestGetImage: "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 @@ class TestGetImage: "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 @@ class TestGetImage: "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 75a151d786..d7b7496573 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 @@ from homeassistant.setup import setup_component 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 @@ class TestNotifyGroup(unittest.TestCase): }, ) - self.service = run_coroutine_threadsafe( + self.service = asyncio.run_coroutine_threadsafe( group.async_get_service( self.hass, { @@ -70,7 +70,7 @@ class TestNotifyGroup(unittest.TestCase): 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 e6f05cc2be..7a97de0f68 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 @@ from homeassistant.components.homeassistant import ( 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 a12b9af0eb..4a2e4fed6c 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -10,7 +10,6 @@ from homeassistant.const import ( STATE_OFF, STATE_IDLE, ) -from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import get_test_home_assistant @@ -162,21 +161,23 @@ class TestAsyncMediaPlayer(unittest.TestCase): 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 @@ class TestAsyncMediaPlayer(unittest.TestCase): 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 @@ class TestAsyncMediaPlayer(unittest.TestCase): 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 @@ class TestSyncMediaPlayer(unittest.TestCase): 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 @@ class TestSyncMediaPlayer(unittest.TestCase): 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 @@ class TestSyncMediaPlayer(unittest.TestCase): 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 @@ class TestSyncMediaPlayer(unittest.TestCase): 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 ced2f512b4..81430cff34 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -5,7 +5,6 @@ import aiohttp 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 @@ class TestRestSwitchSetup: 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 @@ class TestRestSwitchSetup: 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 @@ class TestRestSwitchSetup: 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 @@ class TestRestSwitch: 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 @@ class TestRestSwitch: 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 @@ class TestRestSwitch: 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 @@ class TestRestSwitch: 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 @@ class TestRestSwitch: 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 fd6c0f7330..67d826f576 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_number as input_number 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 @@ class TestMediaPlayer(unittest.TestCase): 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 32d73c70d4..b3dcddfba6 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 @@ class TestUptimeSensor(unittest.TestCase): 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 @@ class TestUptimeSensor(unittest.TestCase): 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 @@ class TestUptimeSensor(unittest.TestCase): 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 e81ce7a4a5..5ac13027f2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -15,7 +15,6 @@ import pytest 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 @@ class TestHomeAssistant(unittest.TestCase): 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 @@ class TestHomeAssistant(unittest.TestCase): 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 @@ class TestHomeAssistant(unittest.TestCase): 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 @@ class TestHomeAssistant(unittest.TestCase): 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 023c42cc81..8dede61869 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -9,43 +9,6 @@ import pytest 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 @@ class RunThreadsafeTests(TestCase): 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 | 223 +++++++++++++------ 10 files changed, 267 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index cf70b7470c..dd5401950e 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.core import callback 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 @@ from .const import ( # pylint: disable=unused-import 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 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _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 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow): } ), ) + + +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 478dd3754e..0b436c4e20 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 = "server" 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 94d990952a..137619b27b 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 0927447291..d4393d38c9 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -11,9 +11,21 @@ from .const import ( 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 812e7b81a7..6538d8e887 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 ef1b56222b..ee439d4559 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 e7e4ed37e0..949da3ad40 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 9991a6bc1f..e35a83bd24 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -113,6 +113,7 @@ TEST_REQUIREMENTS = ( "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 d027087828..87fb6df597 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 37cf0fa200..753d565a82 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 @@ from homeassistant.const import ( 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" - with patch( - "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized - ): + 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=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 47325a6c93..684285a1cf 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 ee439d4559..4c963ddab2 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 26f112bb00..d842ae39ae 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ REQUIRES = [ "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 2148375621..692e552608 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 4c963ddab2..dd38be3bfb 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 37744dce18..ae3ce7c594 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -20,15 +20,26 @@ from homeassistant.helpers.event import track_time_interval _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 @@ CONFIG_SCHEMA = vol.Schema( 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 @@ class NZBGetData: 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 0000000000..84e4af15b5 --- /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 ba4908fe85..8fd4b4fe94 100755 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -93,13 +93,6 @@ 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), 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 137619b27b..8e068c909c 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 8b7b819cfc..68d8a88df4 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -43,6 +43,15 @@ class EcobeeBinarySensor(BinarySensorDevice): """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 6eccdccf8c..f930282ba7 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -305,6 +305,11 @@ class Thermostat(ClimateDevice): """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 e62d68dc9b..8cf9af0e3b 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -54,6 +54,15 @@ class EcobeeSensor(Entity): """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 dd3112b636..6175405638 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -61,6 +61,11 @@ class EcobeeWeather(WeatherEntity): """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 d37422d417..fab028560b 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -5,11 +5,14 @@ from pyotgw import vars as gw_vars 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 @@ class OpenThermClimate(ClimateDevice): 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 @@ class OpenThermClimate(ClimateDevice): 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 @@ class OpenThermClimate(ClimateDevice): """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 @@ class OpenThermClimate(ClimateDevice): """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 02443d449c..3c12eabe8f 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 1460786890..11e11ebc6f 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 d3c4c0d5a7..efdd75b148 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 c547e4306b..2b63840d00 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 0000000000..53146bebf8 --- /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 0000000000..1ea53b778e --- /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 090de51703..3258cf2ddc 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 95550ca099..390069b769 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 04b764e271..3f48e2afde 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -7,10 +7,10 @@ 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.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 a4f6afa163..e383f1013a 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -8,13 +8,9 @@ from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple, Any 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 @@ from homeassistant.helpers.event import ( 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 @@ class Script: """ 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 6dcd8391bf..acfa853d59 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 c085addad4..dbd76a2bc9 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 dd38be3bfb..385f2d0e1a 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 cf2d6d25c4..23b62bb033 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -8,6 +8,9 @@ from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice _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 @@ class VeraLock(VeraDevice, LockDevice): """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 8365ca1a76..b2f1581e76 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 385f2d0e1a..245f91dc9c 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 19140aac93..20d443f225 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 c7098867ee..64b8b71457 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -33,6 +33,7 @@ SUPPORTED_REGIONS = [ "sa-east-1", ] +CONF_ENGINE = "engine" CONF_VOICE = "voice" CONF_OUTPUT_FORMAT = "output_format" CONF_SAMPLE_RATE = "sample_rate" @@ -101,10 +102,12 @@ SUPPORTED_VOICES = [ 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 @@ SUPPORTED_TEXT_TYPES = ["text", "ssml"] 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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 @@ class AmazonPollyProvider(Provider): 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 d377ca7dca..7ac91964a9 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 245f91dc9c..35ee05977d 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 dbd76a2bc9..dc2d31c235 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 35ee05977d..2cf2c765e8 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 949da3ad40..bb29540d6d 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 49e0c46fd5..793c19cc46 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 4b8d696749..9f712ba5c7 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 e233f430cf..ddb4954794 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 0063f1ec37..f207e6dff0 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 0c759f0ad6..cf3e621fd0 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 b9ee8939dc..a7e8aeb8de 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 5bfe85547f..17fa14a9cf 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 ddb109a99b..e7ea23a43a 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 0681d5df38..7d44040742 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 95e26de53b..04ef58769d 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 3e0d4112d2..5ab69d94cf 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 9d2c0a2056..fd5c3010ab 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 2e27beb48e..0626939075 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 e4fc9eb868..c6629982d5 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 dacc428ea2..9ac8d1ea1e 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 20d443f225..45e382647f 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 8e5ddb924c..3d175165ab 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 056930edfc..8f363ba219 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 f79ce34897..4453687b89 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 d20b10b2d1..6bf79e27f8 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 28909f7e05..c9602d7575 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 dc2d31c235..4fd3b062a1 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 17802918cd..d4055a4068 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 9b2e3c697b..7c648d37b1 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 ac36af7fa4..fb2bd64352 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 813176728f..08cac54524 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 25d9a76036..830fc04445 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 9a310a096a..4845c45a96 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 c391fb0e14..f8fd2e0efa 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 fbe13ca857..c7615817b5 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 40f1805d83..2e5dded4af 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 16865905ae..d4c491cb70 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 59ab3c03d9..288b8fb389 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 cf21cbe87e..3567ce71cd 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 d5bcf92a39..ee6b915e65 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 35803d0d4f..8779e051dc 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 597975619e..ccc4f11909 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 1c861aa67e..d84202cbac 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 db1308b048..56018ba777 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 bafe43c480..cf793328d9 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 f36819f133..3fcdcf42ab 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 621faba4fc..55739cad8c 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 e41491c4b0..ebaa564736 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 56ba3fe935..204327043f 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 56325dd40a..f49421ea95 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 10be545f5e..1b0ab33f38 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 9743835af2..63cf0da0f4 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 935cc7a917..79a6877692 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 273cefcbcf..4fb9ab9f42 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 e7d97f1331..8bb8b56cb9 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 dfa5bec3c0..f4e632cb7d 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 a473a23f91..a4543cc4b0 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 2b1bef9081..348f614838 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 e2223fc97c..0b705bddc2 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 1dea1b7e37..756a1c5adc 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 25480ac8bd..7060dbd396 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 5632836bfb..edd6032682 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 54cd9a3af6..15a648167c 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 3e9ad732b7..bc69efb649 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 90e62c7835..63816f967e 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 d627351958..ea29314eb1 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 85da99a688..1ffc34fcd9 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 98cbbc9be5..63c0494c2f 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 9e3e41290e..c5b3a632c1 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 98c609731c..a38ba0bd61 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 a5277c97d9..0a6e254079 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 c11583ed59..629bdebf27 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 8a2a9f7b71..773b52e724 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 3a780507dd..49da6534ba 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 7731f84500..e64e3e61f1 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 d2f8f10290..30ed924a9d 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 c853bc5a83..20fe51c561 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 2342c8418e..9d01b301d4 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 976be85ca9..c062d14f8c 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 0cc875c50f..29366715d2 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 eb1f1d8ca9..b2e7eb08ef 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 52e8e1bec7..e49e45865c 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 45ed200302..d77c32966b 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 d3b0657fed..f3dd46c96f 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 61823564fe..2905bfcfe9 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 a47e3f69d5..6ee4344b94 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 927d9ea941..bee9cefce1 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 725541082e..985f301242 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 d25a2526a5..cc3c3b0298 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 55cd555d98..896ace7ba6 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 3a09cd090a..3e6ee8422b 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 3af6a15ca5..a3395965e4 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 346c1c99f6..f76d521853 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 84a6a6e293..b6776a17f7 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 781f27afb5..97f72f2ad1 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 152c7d3a2d..c6ef7f8f12 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 9a12ba252e..4a04ffa32e 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 abdd240031..b4bc2ff86d 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 21c4efe071..3560b1e7fc 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 926925a7bf..8058088bf8 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 ea1ceaa953..f46c7ba708 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 4d835ed4e7..35368dd6cd 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 ffd550eebe..a10da6e1cc 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 3831982509..6a28b3c30c 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 c2a86f426e..8aa3eacf40 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 ca5312e767..5933eaf907 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 3daeac43da..b15fa32cb1 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 7716ae65c4..78bc6de99c 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 1528f4252b..fe5b8e155c 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 ac42e374fd..f07813b3db 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 5f8a189c7d..2da323f081 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 0afb1b1c28..ec9aec6c65 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 47c7931a0e..89fbb84e82 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 03319aeffa..57daba7fdb 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 ff94522210..4d7dfc8994 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 f26da49d3f..f5ff021b6d 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 9c0c50a259..c6e99b43c4 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 f01fe7324f..10c067d4a2 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 ddd3b6205e..0d6d67cf25 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 9489dc7268..69ab8ee3c4 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 ae7066ea82..3fd533054d 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 da5a644334..1d82dcb5b0 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 5a1bdbf5a4..b2abc40dca 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 9034cb7740..7950cad9b8 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 0f74216b9b..c5cc45d319 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 def2846c4c..e30d64510f 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 7064590bf2..2db35dead6 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 a60355efa0..f81cb17158 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 a210b5a78d..189b685d4c 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 e4e6482484..0046e51463 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 40a2e82d53..36de2ff210 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 3c877a3484..874b24e370 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 4aec29008d..1e5cd41442 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 923a543e82..5142b5fb2e 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 42ab6bfd6c..14b8829fae 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 6969d9bba7..a240599c42 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 90e1a4e3b1..2d550a0851 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 d33a140ced..b2eb3ada65 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 4f167ecae2..a4802c3b67 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 2068b72fa9..92b2aebab4 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 34699d666a..9e084c78e2 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 463c7d03fb..9a2bf66601 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 a95e9c4f68..50b1f9d357 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 40ab85bc1e..3bea097b83 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 7b32f7845a..0e1e0e8cd8 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 05889bdd32..e1d9273b79 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 aa8b584aec..d9e821c82f 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 2ef940f60b..eb19a5c3a8 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 990b39b21a..4c58e090a9 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 7dbe6122ac..8b65d7a680 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 ca304bce88..18282f0778 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 31940c28c4..50c03bad25 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 845e1af15d..56d968189c 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 c2ede62ee5..431afc080f 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 388017f78b..2f764ae281 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 8f7d07eb0d..b0b8c60121 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 bf05d5c7f6..008a1293e4 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 544ac9b0fb..4f3d84da47 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 f8d13b49f9..933af93a77 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 75c1bd3dcd..e0dcb48527 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 3fb9fdc753..c9cdb32e18 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 122d774c26..ac0044c7a8 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 514509c004..241dadddf4 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 21c98d56d1..250adab263 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 fbf7a00f8e..f3ccbdeebb 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 fc13fddc93..3e377f3a2e 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 ed38d35346..9e0ad6c001 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 602dfec801..131063ad81 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 35e6c4a244..47250b32cb 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 a2b21a9e0b..a35fcbcdf6 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 e0a0062021..75d8cfb605 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 7b956dd96c..92940c8c1e 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 16b033df8f..d32206da72 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 46b8fb761d..b9be062d3e 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 5bd488e0ff..c1bf938dc3 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 148e355a3d..bc87b3cd64 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 3ae6b1eac3..7ce52c021a 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 4495cb3c2f..5de2390dd3 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 4684655aa3..36918fa5ee 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 9fe0e4c50c..6d1d444d2f 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 967f04228a..d4ae0c9d6d 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 f4ca116a64..99b966c6c5 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 6f10344986..b0dfc63d92 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 2b008c3c37..3353f1fa4d 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 98d94fd009..0bc242509c 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 466f9da7e9..75acab5860 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 04d3844162..b4871a805d 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 87688733e5..12dbb15220 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 90623c01d1..83833d4f79 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 0cb09e3fb7..80d946f486 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 75fcbc4c55..9b3b00d20b 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 ba68ce9495..824e5bef7c 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 d523bd72b7..870681fd4a 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 e6f1c5d782..4dffbabd21 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 86d2d69cf9..5b5f94f7c8 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 b2b60cff95..b0910f1653 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 2ae2006512..c62e1e356b 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 c69a66d43f..ddf69d0d41 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 b34aa08951..6c5405c75e 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 3fed307aed..7509e62762 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 e6623b8301..22055e347a 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 21f76c3a31..d73b331d5f 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 6d13c79bce..1065b94c12 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 43987cce2c..bde6476212 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 aeb3b48311..914c8f1556 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 452d1c4c47..f0abf8c7de 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 ec7f1fe707..92a9197650 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 9c2e1b2ae4..53e2dbf2cb 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 32a57cf20b..5633880be3 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 9632906a25..930047065c 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 4a3eefc135..2c911eb04e 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 fc60658a3f..6ff0c7be0e 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 48a73dfb03..e8aa3ab51b 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 85bb982d2d..0df3b7a850 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 f4bf021380..3655ce22ba 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 e458d30073..6ecc9efffb 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 4a3695e7dc..438891eca9 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 e9a0e7b101..5b445dd309 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 71600b3111..1bb8e7353d 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 3574e6254d..0a5d131656 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 343a21ff07..638505d5dd 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 581b0e1415..07a9cde900 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 f76bcd2746..14e0a6a487 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 28f061d26f..f28007ba55 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 e358067629..1d0879c291 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 6a6316d80a..f550cb75c5 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 1e010bb06e..e6661ca6ce 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 c37ece4ebd..eece29e167 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 0ee0e81143..311904166d 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 827bcb167c..d0651c7fbe 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 a5af541eee..ba6f4b1f43 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 76053f7508..a5dfaf4027 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 9bf3ba09ce..e7b0387698 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 0d00275200..4e5531ab4e 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 7a0bf76e0a..d4026e7689 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 1a5b547e5f..17c54a3976 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 9ed95597e4..b02149d2bc 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 544ea86077..ff063d6b7e 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 a63d4b292a..4ec5a4fcb2 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 b05aa956b4..b2c44c113e 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 84a98ca033..c488bf790d 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 b8a40c3fc1..19d1ce0aff 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 9ee134d417..ac507f59c7 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 63f929754d..02332c9fb1 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 b2aacbd48a..e6c1fee2c9 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 1ed18140bd..a1e2896656 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 19f232ed66..35c27b7ca8 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 ac1ce2893e..88a7ab5a33 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 98d628fe07..e7b59b0713 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 8f737e2e1f..c7e919c95e 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 e50989a15d..f1d91879f1 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 0e20a509d1..17e9f973fd 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 5191ab611a..c1b0cd2c0f 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 d3781f81d0..b86f4e26b1 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 96d792196c..5ea7cb2fb4 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 39ceca41d0..c8bb89c71a 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 d53bceaa14..0f81ecbf1b 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 e4d3622a56..9d59d5b991 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 41fb04c845..283ea3c45f 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 f2110ffb2f..feedf3be60 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 6ee78fec56..c4c892e88f 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 83b4241284..74dd7cbbf8 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 bce6758b0f..8fd19f6b03 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 d593aec46a..f7939e2b02 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 77f3c64752..f7aa53b0a3 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 a2c2ae0437..0b5e3c0df9 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 4ea04de9e0..e439e8d7ed 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 6600e46a4c..96df8c4e08 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 621bca8c43..775d208c1c 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 7315e3c7c8..f1c030125a 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 861abe0b46..a4d7cd5068 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 3f3f2c25d0..d8878c8b35 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 4c7e82ecfe..d72cc992f6 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 ff91693021..d2e016cb5d 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 c8ac0d2e81..bc9c71c5a6 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 190e5860ee..64076434aa 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 ec48e5252a..30571c3386 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 ff61ad0e05..b23a101ca4 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 cb3cd350c0..8b9621b423 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 eaa168332a..f4113f85a3 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 6e84045820..77062cbdd2 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 98ab803502..a3c2389478 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 b35d5cb185..7bb828cacf 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 f039e50914..cbfd79671e 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 a5eefc5af0..4974812825 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 7bfb87ede4..1e9569e850 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 1032b5eaf2..20a49e834b 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 aa99e20a4d..195227ca24 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 a6a1d2b8ae..0d4508c26d 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 6bfb8abbe0..66cae733d9 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 1c7ddbd65e..b25134bb79 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 142261fe15..217b175555 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 b8e622823d..a3ac10a1c6 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 4a90e9c977..2f222b3c16 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 eecbf0edd6..6bd6494261 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 a957db0675..af4427c5db 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 40572f82ea..00c7b7a19b 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 2d34d3b4e7..484886aff2 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 b59d562282..7c877f5c2a 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 0a11aecd07..b3882c63c5 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 eb9ef258a3..fb21a43356 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 e26e2e1d6e..0f2bde253d 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 bee53c89cd..78917a5351 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 f2bb0822d1..8dcef17fad 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 d49e05a541..9d563719a2 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 e098995862..00789c905c 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 fa0d437a70..a4a0eb4d3e 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 ea0abd87c2..55a3449f4d 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 9f3c20fcca..6b0492881f 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 d9fae3fe54..4164283f9f 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 5266b81ab0..037df20b35 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 b612c3a9fa..b4c03047a7 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 ebb0895bd7..c0ab61d856 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 70f6f6a3ce..2a66028131 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 3c350e7573..260e54e65c 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 2075f88ded..40c8c7c359 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 cdbbffb8d3..f2929fb655 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 b50c7f61dd..9d644de444 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 d9898a71f8..035354c969 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 2916e81ce4..4ba3a61d8b 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 a3d5853541..3dc591cac4 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 7b43ec44ef..667a578918 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 0bc5586445..6db8b041cf 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 70093df9b5..14b0d7b3f1 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 3af23be4f0..5d559cc60c 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 54fd155b55..2affcb8ee2 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 cb37dd3036..9a3e478d10 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 c4e1bcc28e..3aeffd025e 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 6d332a28bc..eaa0d10622 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 efea5ce0f2..dbe8af0b41 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 980c227944..e4ac9c0897 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 df492d136f..6e575ef17b 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 25e0253689..e883aec371 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 5f2075a0fd..d3924ee61a 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 8df144a0f8..7a8e395568 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 58490569e6..975a3128f3 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 4d84c27cd9..8771ada45e 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 d2ab3ad449..edb77f1dc6 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 25d0317078..a415b0e310 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 f3a7121c0b..4a96e9828c 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 9e0f387a7a..20767a0d49 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 a1e2c61683..e689cb859d 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 c26ba27a29..4bdf43f895 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 feda5da732..df936eafa9 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 e233b5635f..09ae235e6b 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 287777e2cc..9808c45aa7 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 2015b8ea73..31e00d0fce 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 a71fb53a5d..d71674fd40 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 6362e67931..eaddaf49b8 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 5fcc2c5b50..c8821f3b17 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 869ad2766f..d910e7bdc7 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 891be6b218..f2d9a33856 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 28c9ea1e95..6e011a43de 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 d83defbbec..018ea4ac16 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 0547628b4b..c3b1e27c77 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 093ccbf6a5..47759759a5 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 7392c931f4..caf422938b 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 5961400e68..da15d87ab0 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 4dc9e2cb7c..035b61d0f2 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 dc71e81ac0..72521e67af 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 0dd0f1eae8..759e1b78e8 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 c26b19c636..86bb362f8d 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 6f05125661..ec47deabc2 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 2f6747ab4c..a3aa3ad3fa 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 fdc1d2943e..7b6653ba83 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 220f2af203..a2c2e4b11b 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 e65aab2b69..1ef84b7450 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 926f73fa4d..eb3626a315 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 8e4e974790..ef6bcbf92e 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 9e959f35c9..422a79cd0b 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 417616161e..41e45a9e57 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 0e8ade339c..a3cf7a51ed 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 d87d1abca4..6172de132b 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 b7edd1f6c5..78542bb7b6 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 9f1595ebd7..888c653301 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 3b2d141403..76f15f3bdb 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 8c684d495e..ef138bb2ee 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 e4129af39b..397373499a 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 783907c022..f79b0f5b35 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 99dd488921..c30a147342 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 bbf22918a7..72e12e78ba 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 951dd3ff85..47bdd1ee0a 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 2617b3e206..78ecfd4efb 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 bbe9fa8ad0..5bf63f2b09 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 5a85b6673f..dcafe908d5 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 1728aa5061..3f3d9d85c5 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 b09c880938..603f755fac 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 9eae371070..a890ec3937 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 131d1a23b6..a8c2755aef 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 83805692e4..f6aa8ccb7c 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 fb38b41f31..de5f5ff04d 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 62eb96967f..88e7585f80 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 a26500f69a..4b2456f0df 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 f8b42fabcb..5eff655e80 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 19bb079c29..28ed3f5203 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 10a5bbcf86..a2505427f4 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 7dc2e0d751..064f7e1ccf 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 4c32b88b2d..3730f01622 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 d11cf0b2f1..b15799b54e 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 08bcac6790..988e2bd1ed 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 3393022a36..bcb2b53f08 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 e66050fceb..2f46e6e790 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 14a503f33f..ded748be8d 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 be2eb07a23..46a2d4de20 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 29a7a5513d..0a76628a5b 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 b515d65a14..18ab9036c5 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 cedce8152a..e8e3ad8ac2 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 60be8f275e..c546030853 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 c6b6238703..ac201fb00a 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 b176774839..22502956e0 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 3f0c97edfe..cca4a54bda 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 5262fa4837..e66d5c1820 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 20fe6fac2a..41e3d0dd6b 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 dd8da40efe..72be440d54 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 dffb4b5266..646fc1a3cb 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 26d6c21f3a..112b58ba65 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 344ec82d97..bb6b18243e 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 451a6f3e33..cace2770de 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 4da58cdfc4..d1501a562d 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 205072055b..7f830cda25 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 ff7da7190d..e8b593b314 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 6534d927f1..41795c117a 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 4ca1db564a..2883c9caf3 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 9ed7a50a8e..c0bd0823b8 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 6c78897162..12d5297c01 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 81cd133845..d9ec004e6a 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 d26d7d9530..e64d18c92e 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 5316935c44..0188f405e1 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 4005e51e37..e041ba2f66 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 9ea1a6f0c5..a467518c04 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 a28096c5eb..1f5b1eef93 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 41048683c9..2dbffd829f 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 71e1a81135..886535555d 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 bf6f8fabaf..4df8ad8442 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 134d85fa17..0e42cd7e53 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 f9fa1cab50..64760338f3 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 d03679ed41..2add866355 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 a6c49b3c39..79428f951f 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 426d0faf86..2652e33b76 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 b485458be4..ba043bf2a7 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 692e552608..ee14ce7d26 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 f5d358854f..f5624e33ed 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 1e84b39a36..c08b4e4c88 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 8545db90e2..5bffcf8e92 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 827d961a09..16ae94c212 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 7f6c4fbd93..1d51dca42c 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 b272a299cf..12d73623e7 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 10e4bde103..a52aca1ac0 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 c7ef2b8961..54fa59135b 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 9286985654..9a05f5a9f8 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 05efb845c1..85e70c78ed 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 ea6befe498..ed899a0438 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 2b2f84836e..bc37363350 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 2324a861b3..612e7c19f8 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 2ecd66910b..93f01ac2e3 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 85c6231daa..8c95ca4ad4 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 0e5c4dd1ff..8994223fe3 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 e27f594b0a..8d271d5a95 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 e3d6d19b80..80174b1a83 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 de4680927a..1205b53cca 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 aa07911a69..47db73ce0d 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 50a93fce20..56b5a1b818 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 5acd5bbdcd..ddb51c9e68 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 e874ca2889..7d41924347 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 beee3137ef..b7440f655d 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 2df50699a9..c1b6659e1c 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 e795c8aaf1..0d36cd6616 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 a1986b2bf2..b544883979 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 8fc90b0bcb..93450b0016 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 5fa9936372..840a53591a 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 5626e24448..e47b7c07d8 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 1ff997372e..20933c9b2f 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 77e5a524aa..5d5ee19538 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 b870ff6630..213679b320 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 536848d3ae..8701424ea6 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 0e17f33f72..fe09461bc8 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 4e37544a99..b912a80f75 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 b49932887d..7f010ec6d3 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 c624acd73d..7e01f818e3 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 e75e2caa37..aa1e2eb742 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 a59a6352af..3318ad3438 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 71553fabc8..8b0c5acc72 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 baa6551cc7..c1360ecbe3 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 0caafd7e27..e747bf7739 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 93b19470ac..a1cae2df1d 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 8a6e8ec611..b7d5f13204 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 82f32c3440..83091368af 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 9c3b8ad33d..da4d15c28a 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 3ee3b18993..af709c755c 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 99ca3cb1cc..7e085d0630 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 e3675db04d..3755746aee 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 04420d5c4f..735baf58e5 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 5c5a095c8f..525947a8c7 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 8f3d88b58e..45eedf8a15 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 8cb58a7b74..0e168601c4 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 ee7645653e..fe7a92bc27 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 70aaa11241..a318129214 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 0380acba1a..b207bd07a4 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 1a2fa05568..5fe5f743fd 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 1258159953..438b61136e 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 9ffc0215fd..069fc238f7 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 08c9932c36..e7ee2d2dcd 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 bad39a1cb9..6586496abb 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 827d406a1b..2a400754b4 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 6be24fb5a2..744497c22c 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 4542eb45c8..3d16f0a57e 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 c9e69c44ec..3d055f8f97 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 9f18d2849f..a88faaa6f3 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 e7f078a1a0..77043f3713 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 920e56fba7..21231c2787 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 bad90d9e82..b112a9ea4e 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 67b5b0e2ee..64f72986d0 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 17b11d6aef..1dc16fdba4 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 15bf40e63c..e6cc998352 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 68045ff058..09087663b4 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 c34e1458e4..77f6a22e69 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 d23b07b275..1634676a60 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 33c93bc8ac..c958d23e72 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 066f3270cc..fb6daf00f6 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 ffb01bd560..2febfc481e 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 00075d4485..2d8c6c7107 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 7fd27dd7ed..5bb116dece 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 d86ec38ccb..f6c2371218 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 f042129583..56128f0e88 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 3c92e840f4..65fa6b1bec 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 e8ebeb102e..40421674a4 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 f37c769d20..a56f0cadda 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 ffb86d4a5e..fae9e616e5 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 95f944b708..4dcb53e98c 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 d9281f08ed..4a17f93351 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 276346ae79..2ec58b8612 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 ab03f1cf7c..632ab82918 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 dd58cdd416..a98e828a52 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 c6097a01cc..9c7f165c6d 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 0cfb02e81d..69342df235 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 d24b23f64b..c28d27c1b8 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 c10be48f3f..cee6447abc 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 65fd0f7de5..51bca8fbbb 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 73f4eaed7d..9dee62697c 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 0b158b9674..8c6c9f30b1 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 112fca2419..25ece71c11 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 b89947343c..2ba00671b4 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 bc4fe97bc7..529d7990a8 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 fe2387744a..7f386464dc 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 432e729ef2..e7e06479ee 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 68e8337a33..a15267b7d8 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 06c9338742..5d0cc55570 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 e66f94bdcc..6d7c66f609 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 186e071d25..d31b9a811c 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 8bc343e1f0..9ee9692c65 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 d2cba92925..cf50b8029c 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 0b1579a139..4845aa16a3 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 7fe8bba687..089e1e60a1 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 bfe7f449ca..5150ddd040 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 67b1033c51..012ce4f014 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 dfe4952e1a..7613f9e2a3 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 d98adef87a..19e84365fa 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 b06874149e..3aa046f234 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 6901847bd8..3c2b67e342 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 cd6111ba9d..658173d3db 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 cbde894173..721a57e782 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 8e068c909c..d4f2ae0517 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 c399232f31..1069c0bcdf 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 389eca09c4..8c2da09026 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 11c2023632..f72984f9fc 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 fcc9265ce9..0f2faef86d 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 9746cb168a..d07f9746ee 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 2427ebbfdb..99a8a1e270 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 3aa356823c..a035657a09 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 cab1228aa5..309284e5f6 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 a8b4893c99..73aa818a2a 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 335bea82fc..708a3fb261 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 54d5ebe5f1..c67fd4afc0 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 797a5432f9..98a14d877e 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 8bd46c3dc3..2f8398531c 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 58a2871e02..19247e1a7d 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 278638caff..161ea29b3b 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 51e77959d7..f649e5467a 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 b42180c726..3000f9fd17 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 1cdbb4ff48..01c3fc270f 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 300d0ead4a..18592124c2 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 b61c710082..7e6879de9a 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 437bd3bc4d..4cbcd8321b 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 83d70830b1..2e5c120821 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 5fb850739d..c41d4ba46d 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 47a4a4b5f8..c113e6034c 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 f02d416c7e..a34c49645c 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 eb8da25bac..87e16f6298 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 9c062482a4..da2fc20510 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 4907cb462b..6d2944282b 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 30bde9a297..79e3677d65 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 f12fcf4220..2683525e7b 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 002fdb6327..c3f8079ccc 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 b911aaa57e..bb421c725c 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 4d07f2a3ce..612fc32c01 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 354eaf8b31..c5503976d3 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 25b36c798c..37db2b3135 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 c184f35734..cbbaa562ab 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 8f5040152a..b241cd6db1 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 fee815a7e6..f1330c2abe 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 2cccf32f29..396afe30dc 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 9ecfa88053..cdb09d6606 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 af8e802c5e..b11eca2b08 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 c6d3b3458e..55baecc486 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 7256239933..f1f8299918 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 c9d35e9d2c..4979fe29e0 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 5fe585dcd8..5b9e70ebcf 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 f15defd63d..df9a9c7512 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 14af98cfb6..a894285f72 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 999f574071..0b197d104e 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 ced930fc64..238191a934 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 bbdb49ad40..bda260bdff 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 5d6cd4b038..a75a8ba9eb 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 9dbedad1ff..47fc2f3a6a 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 fe93bf0244..b8aa5c7430 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 3f32a61c08..1f06daf062 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 3a8959f1be..924a9b86d4 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 477bcb105f..f2639b31d1 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 058ad0c5e8..5064357a7d 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 7ac91964a9..34a296b0f9 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 71ec8fcbc9..e033e82d92 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 1f905b103f..d5443787d3 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 88322708b2..0bee2baedd 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 d2ed380d68..ccff3d3357 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 7fc724bf90..fd0a0a99f5 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 e5fffee131..5914f65ef6 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 c92f6b2a0b..7bd9287645 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 ce2dca9e08..899ee3a8ae 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 4667e9b831..9404153f4e 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 716f383040..687dd05b61 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 9424e5f3a1..e69c227f14 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 c0367c4790..e42b37195a 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 c8825f4ac3..a080fac112 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 ae56b54ce1..dbbfebeacc 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 e1becfd193..b80e4de504 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 ec9807d4e0..989070900c 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 56a3c39b7b..51ce17c500 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 d565a5d336..b6334272f2 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 74bdb3d8b8..528e9ef35f 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 1ffbe69888..f9bd8cc31b 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 8763234c5e..0112ca2846 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 cb148c9219..deec8c23ab 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 776b8444b8..9b49f442d1 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 813bcc27ac..1813d782d3 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 945464dbde..61da7b4ff3 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 b2a645c88f..b326481c55 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 ad6c71bd19..f689cd4685 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 45ce2f6a7a..0bf49d4b90 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 47b36c1229..6a5ac16529 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 dfe9a8e8e6..ca354ff233 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 02718396e5..282d86ce41 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 fa704a6550..3ef01a4431 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 b4ea3c2d38..41fe28defa 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 dfa22fc6e2..6ed947e5c8 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 1dc8f5255c..689703302a 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 cbf2833a4f..d303ac221d 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 cf26955b20..96b337def5 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 b972152aea..4dcd071570 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 f8e6e1bf14..d5cfc48815 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 46337918f8..6be4569062 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 893a1f3469..a3cb97cdc2 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 843fd3d13b..8a005be52e 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 38fa5fa089..10dc4bffcc 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 ea16d626af..ac605f42b0 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 f9fd7f242b..3d2836e180 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 ea3a33d55f..8f0caa7c54 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 361802f312..6f5a4b7b64 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 18b587bac9..515f3dcbf6 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 621da91f4f..8b5bf65afa 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 b2e3deb400..003ee804b5 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 421eadca51..b3fdc2825a 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 2e1a8d826c..b6ec6b4ca0 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 0bb36c1317..1314b91f98 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 58fddb7a3f..e15d86e811 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 a3ac3af985..d3942ab4a3 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 23fad3683c..93361d41e2 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 e800bd7266..c90342018f 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 7452790cd6..f2635936cb 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 291c774c38..91ed6f55ee 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 3a154b857f..14ac59d462 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 02eab03c8b..83b50684fd 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 d4e799c2cf..35422cf09a 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 bc0235ec5b..ae32083da3 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 5a01d9f9e2..3160c4cee4 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 a08c0a59c0..6d636f36b3 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 1cc25d93f5..497347671a 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 eba60bc6e3..e3ca9ed72e 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 03aa5c0a1f..4ab05f170c 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 572d4b04b8..5b59d3bfe2 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 91b7e7c5c0..b35df8ee7c 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 4cd7a46773..567f6f0e51 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 2e81da3409..a6972e8881 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 5827f307ec..50d9ba3163 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 366a5eef0a..f514a95c04 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 38a320543a..41d80ebccf 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 ae124d6c03..1f651f746e 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 ce00bcbc88..1c3d56fe7f 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 1314fda509..33dbb40f78 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 d2f9e90c41..79637bfb12 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 49e476a687..3dab05942b 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 20f4cc7f54..387576f884 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 735a1869c3..ecc6198cb8 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 0f8b586a9c..385df6a506 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 f285f81f27..a00315e106 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 b4173ebf0e..83b6314c4a 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 307f4c929c..840984ad07 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 8ee3de2d77..654f99f7ea 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 e55131306d..31fcb1d7c2 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 1fc849165e..eaf1e66cff 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 cac1a5f18a..72635ca641 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 d6b18d6cba..f54ef55ce1 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 99dcdbd0c8..c91d85fecd 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 e52fda3408..27e33c8160 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 0f28725158..73daca18c6 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 b9ea4eb276..204a3605bb 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 2f3b3b6e84..453ae542b2 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 94f100abe8..b15024b545 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 a2a45826d9..79e6f8e557 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 a108f5fa98..ea743836c7 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 d35b1d8c90..3522ba0405 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 a790a6c453..c507e07c71 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 fcce2e52a2..d7dc76b6ac 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 19836ffa67..00dda3ebc0 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 9c2b7bcae3..4de7af3f86 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 01f70af4a1..0bc14a56e7 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 565a459818..a45a59e410 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 8d42cde1c0..9a884fa010 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 ca3ab0bc88..1e99d4b288 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 04ffb48f39..328285ab67 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 1c8e847698..79fff1b2b4 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 d49b528018..cc635082c3 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 2ff29a27f3..53f3f6c646 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 9cc50405ba..b02a9db3fd 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 14afdec3b7..220a086e0b 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 8a6dd7fb36..392c45ea88 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 f341fd587c..afc83879cb 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 7f431ba92b..777138d44a 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 c50ba514f2..3391533b08 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 58f5e15cc1..afba0e3830 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 0e60c957d9..76656e9936 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 c8406c9d08..20a35f1afe 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 279ac3b103..e7d35829ff 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 ab32a64e67..4071178c7c 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 d7282317d9..7c93000790 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 fab670627b..93a334fc98 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 8d6082d74b..98a852dea0 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 482bb94ac2..689a6678ca 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 4e43270a5e..c7c8aaaf16 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 063c84d4ff..ac07a2f77a 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 107b435150..f3aa540527 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 d0f358c590..11c5a676bf 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 7edaf9ba97..c81b6f0a0b 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 3d26e8315a..5e40c89369 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 bd620d4a18..1824ea2db4 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 76a506faee..1cf2c61001 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 ff67748d64..ff582b33ce 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 7a6b4e2efa..dbf1a941e0 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 5f64e661a9..dc87b6c2fc 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 615ea9ecd7..5f6584ce25 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 3fd00e88a0..721f7037fc 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 9ce41b5986..fbc788d252 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 e6bcd6dd00..39cd0e0f1e 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 5b8b4f521e..7c0b36b50f 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 e0f85757af..f299e02e2d 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 e3efd8c833..e1cdacde6d 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 7d3e2f22d6..ffc82ee13e 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 6ad348176b..638d63cb48 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 d847c6df24..d9fa4ad569 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 2e24100edd..1e24895af4 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 9bd734fe09..64f1d636a1 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 2bd4571ef9..c2fa31d7b5 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 491cce7407..9d2e675c75 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 eb553fbe73..f0d5d54c8c 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 8719138f3a..1432d2d21a 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 ce600473cc..ca2059a4d1 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 9c83056f6a..cf16d587e8 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 d1acf936a2..4eebba28ef 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 f96afa1811..23fac51a34 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 b235385396..3fe8f129b5 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 2174dc275b..00c843d2df 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 80bc795b53..f64182f6e4 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 e721bb669e..2713004343 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 39ffe76865..0bf29beb0d 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 f886e84254..664ae86144 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 189ac69075..3711d7cbeb 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 be44a9d8cc..cf349a2057 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 d182806c4a..ecbeb002f0 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 515bd68d01..805dc6638b 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 ac72d10f07..3e066b4859 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 efa38286e7..2cf463d1cf 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 3a58d80f64..62ce608a91 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 9275ef3496..eb26d6e36b 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 9aec23a687..d4446b271f 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 1019717838..5997916e2c 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 375baf1256..cc2d1b6c2e 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 f2ffcfbf8a..707b7f2786 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 0d1c116786..d1ae97b550 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 59f4d1ca21..7a470037ba 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 5c77f9ecc7..497bdac656 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 8dfbb8ed96..69edc40b15 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 4f9b0f4d12..51ecda9140 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 47153dcf17..c4e3a2c97b 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 b071b354d7..1d9401f6cf 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 9f1f4a7200..783e23a817 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 655ba019e8..e8e36d0446 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 b2f1581e76..120ec241d6 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 7c895233f7..38ea8c7314 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 815e7ff9a2..3f35b0dc9a 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 53cc96be38..d52380c102 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 e145b26b0c..99857fc2f7 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 e5f55b20dd..9f7c703fe4 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 cce2307bc4..20b2ac347f 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 c65204d78e..682405a375 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 a40b0e8c7d..0dfc9f1ceb 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 1e0f1c71df..7894eb2982 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 6f0b4ae5fd..7a079d9ccf 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 db068e3505..0b210bc780 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 e7c4bac4ab..a97c9d637e 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 aa691d7766..3f75c39111 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 5f5461f2d6..0a0afe3d71 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 920ee1120a..89b0ac591e 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 dc689f8d61..ef6dbd0647 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 4b692c669d..8ce03e2e8e 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 e291777483..7ed6493b84 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 57aa663a34..e2982bd014 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 8896f34f97..20834bf0bf 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 d40baaca13..4cde3f764e 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 09ae4f812d..85bcc19032 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 7008c033f9..568ce57ed6 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 384e61aed2..f93d8a5e29 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 7c30ad6c5d..7cdf8bea4f 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 4dd2f92628..dcf908cd60 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 bc630b2947..9826b11ec4 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 1902df1060..aa863bcff0 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 6040c8655b..1566366362 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 cddfdc5dc9..acf9c38e63 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 c3da00ce95..7472898b7c 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 726d9f13ed..d38b69f224 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 ba8712f057..4b407e9523 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 2da33f942b..8f7b72491b 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 dfc116c97d..467a98a666 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 3e7c626ddd..b112bb7771 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 c778ed1049..1c20b88422 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 d14c9db419..8309f03bca 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 505447f454..90a55ad48e 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 189b936510..a7bf14454d 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 2fbe16a6e7..bae5247ffb 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 5baf928352..79f4ce6c87 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 ee8ed2f6de..7cf061018b 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 71750ccf08..9e800dc2e4 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 d358710050..a607cda511 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 36da259f82..9eeddd357f 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 d7e0d0d732..4c01cce2d3 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 26940a57c7..740eaf3ea1 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 3d2c3a5e91..2125549750 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 4ee13acf64..290c552309 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 7b786c7bf7..05e979ffb0 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 5a277fc7ce..bacb9fc330 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 7769026e09..ea36c4921c 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 6c633f848c..91267b4348 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 7f622a1e25..66a546abdb 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 061d2b065c..3d27d5bd39 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 1a75472b80..390ff74272 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 c7b5535d03..b68649525c 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 bb7fbf55cb..461f3e2433 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 7f06ddddcb..d49004cc0e 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 c304860159..482951e156 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 c0f100fa62..5959fba5fa 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 ce16e1b523..2b95a9ec0c 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 b846c95f5f..e24e453783 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 1461a54d14..39f016e9d0 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 4d1a55eaa0..79c89406fa 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 f0f6389a06..ab8a20822a 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 6382a830dc..32ee696432 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 1e4076b843..3968b0e294 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 9e58713792..ff9e64ae78 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 e9281fec3f..7a8cdcf6c6 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 9d371fbabf..c29a97e857 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 f88945fa28..9268a50a14 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 | 76 ++++--- .../components/geniushub/water_heater.py | 107 +++------- requirements_all.txt | 2 +- 7 files changed, 283 insertions(+), 255 deletions(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 45f3f91cd6..d9f6c877cb 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.dispatcher import ( ) 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 - async def async_update(self, now, **kwargs): + @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) -> 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 @@ class GeniusBroker: 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 @@ class GeniusEntity(Entity): 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 @@ class GeniusEntity(Entity): 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 105a03bf75..33458d049a 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 a856e48438..f27b1cc7f1 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 @@ from homeassistant.components.climate.const import ( 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 @@ GH_HVAC_TO_HA = {v: k for k, v in HA_HVAC_TO_GH.items()} 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 - entities = [ - GeniusClimateZone(z) for z in client.zone_objs if z.data["type"] in GH_ZONES - ] - async_add_entities(entities) + broker = hass.data[DOMAIN]["broker"] + + 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 @@ class GeniusClimateZone(GeniusEntity, ClimateDevice): @property def preset_modes(self) -> Optional[List[str]]: """Return a list of available preset modes.""" - return self._preset_modes + if "occupied" in self._zone.data: # if has a movement sensor + return [PRESET_ACTIVITY, PRESET_BOOST] + return [PRESET_BOOST] - 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) - ) - - 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 feedf3be60..96497388a4 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 82db3d4224..2f5d9bceb8 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 @@ GH_LEVEL_MAPPING = { } -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.""" + 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" - 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"] + 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 @@ class GeniusBattery(GeniusEntity): @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 @@ class GeniusIssue(GeniusEntity): """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 1086160e77..cd4f536e14 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 @@ GH_STATE_TO_HA = { "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 2cf2c765e8..a0f64eeec7 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 23fdb38aa0..0a95de783f 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -5,7 +5,7 @@ import random 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_CUSTOM_EFFECT = "custom_effect" 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 ab4545256a..197e424ce8 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, + ATTR_MODE, CONF_HOST, CONF_PORT, CONF_NAME, @@ -19,7 +20,6 @@ _LOGGER = logging.getLogger(__name__) 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 8fd4b4fe94..b752b82d08 100755 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + ATTR_MODE, CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM, @@ -77,7 +78,6 @@ ATTR_ROUTE = "route" 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 598e376561..cd791434f9 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_MODE, ATTR_NAME, CONF_HOST, CONF_HOSTS, @@ -47,7 +48,6 @@ ATTR_VALUE_TYPE = "value_type" 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 007ed6517e..9b4d5a961b 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -7,6 +7,7 @@ import homeassistant.helpers.config_validation as cv 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_VALUE = "value" 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 41d78e6e7c..1b4670cf1e 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -7,6 +7,7 @@ import homeassistant.helpers.config_validation as cv 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_VALUE = "value" 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 ed26db3d49..d183dcb0fa 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -33,7 +33,7 @@ from homeassistant.components.light import ( 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_COLORLOOP = "lifx_effect_colorloop" 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 12484a655d..f7ed3a73fc 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -8,6 +8,7 @@ import voluptuous as vol 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_SET_CONFIG = "set_config" 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 93fe285dcf..f284b2eda1 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -27,7 +27,7 @@ from homeassistant.components.vacuum import ( 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_BATTERY_END = "battery_level_at_clean_end" 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 0c14596365..a32c375ac6 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import DOMAIN as COMP_SENSOR from homeassistant.const import ( ATTR_DATE, ATTR_ID, + ATTR_MODE, ATTR_TEMPERATURE, ATTR_TIME, CONF_DEVICE, @@ -28,7 +29,6 @@ import homeassistant.helpers.config_validation as cv 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 77b0bf9b31..60042b9286 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -4,7 +4,6 @@ import pyotgw.vars as gw_vars 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 5f08d0a475..9d0610c139 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol 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_DUE_IN = "due" 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 0d4d373b2b..5246fb49ee 100644 --- a/homeassistant/components/wink/lock.py +++ b/homeassistant/components/wink/lock.py @@ -4,7 +4,13 @@ import logging 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 @@ SERVICE_ADD_KEY = "wink_add_new_lock_key_code" 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 c6ca6db32f..67dc12565d 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -12,7 +12,13 @@ from homeassistant.components.fan import ( 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_MODEL = "model" 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 5f79652621..7fa1638253 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -6,7 +6,13 @@ import logging 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_POWER = "power" 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 b47cdb9816..ab63e6fb31 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -11,7 +11,7 @@ from homeassistant.util.color import ( 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_WHITE_TEMP = SUPPORT_YEELIGHT | SUPPORT_COLOR_TEMP 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 9aa4544f5c..7d8a68a970 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -271,6 +271,8 @@ ATTR_DISCOVERED = "discovered" # 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 28dade82d9..1af7fc0490 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 3b962f38ca..885164d7a3 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 08f20778d7..ae9531456f 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 93a75d44e9..67553ef608 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -58,7 +58,7 @@ class OwnTracksFlow(config_entries.ConfigFlow): "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 2790d9a93a..59cb270c61 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -3,5 +3,7 @@ from homeassistant.helpers import config_entry_flow 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 705ac59b98..25f0545c34 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 756eb65d4f..3a7223ade2 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 198a5e9cab..7206bea110 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 2ae9cadf1a..3d5b4259aa 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 0f7b3c9ef8..a8a55dea48 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 a3ca8fc762..54c9f81500 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -176,7 +176,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): 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 8c7edb26d4..2c7c71d7a6 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 cea3146a3a..dde5165c5c 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 cc3f1f2372..4bd7591016 100644 --- a/homeassistant/components/traccar/config_flow.py +++ b/homeassistant/components/traccar/config_flow.py @@ -6,5 +6,5 @@ from .const import DOMAIN 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 1408e05e73..dad8e0bf49 100644 --- a/homeassistant/components/twilio/config_flow.py +++ b/homeassistant/components/twilio/config_flow.py @@ -9,6 +9,6 @@ config_entry_flow.register_webhook_flow( "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 145b725fc7..1873cd7dc5 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 3d4a03fb0a..37b0bec207 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 378be778e6..16592c9a8d 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 f67ee2fb75..7afde3e5f7 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 7a5f0161fb..dda6c1f4c1 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 2f6e6c1b3e..14d982ab1e 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 d8f769a3e2..272fa28905 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 99fed7d5d6..7cd2134988 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 e15acdaf5e..31dd5cd63d 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 94d885592e..369ecb69aa 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 b9785068f2..aa463392e5 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 25c11a9fd4..e4840dae86 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 8e2fa7e3d5..c7de294369 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 82d20ff78c..e9e2c3b7ea 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 80642a373d..622adead80 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 d2f842956d..a64e8cf7fd 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 b07658e72d..88a472716c 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 d648390260..a4e84e8310 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 db7e89dce8..43ddc888d2 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 0e840e1d00..97c996d9e5 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -60,7 +60,7 @@ _LOGGER = logging.getLogger(__name__) 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 cb4ecac61f..0bc54519ce 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 f5cd03882c..2bfebf145d 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 1341bd75d1..6f63fab3fd 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -4,7 +4,7 @@ from typing import Dict 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 7b4d1f96a3..4008e6a17b 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 db63582852..5b43289e40 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 @@ from .const import ( 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 4522ac4254..eac1473507 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -10,6 +10,7 @@ CONF_CONTROLLER = "controller" 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 b29b088a81..ffea98b905 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -36,6 +36,7 @@ from .const import ( DOMAIN, LOGGER, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from .errors import AuthenticationRequired, CannotConnect @@ -50,6 +51,7 @@ class UniFiController: 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 @@ class UniFiController: """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 @@ class UniFiController: 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 @@ class UniFiController: 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 ad04b8a0eb..48b19d7bad 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -26,7 +26,6 @@ DEVICE_ATTRIBUTES = [ "ip", "is_11r", "is_guest", - "is_wired", "mac", "name", "noted", @@ -121,6 +120,7 @@ class UniFiClientTracker(ScannerEntity): """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 @@ class UniFiClientTracker(ScannerEntity): 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 @@ class UniFiClientTracker(ScannerEntity): 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 @@ class UniFiClientTracker(ScannerEntity): 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 4f757102d5..f0183a7ecb 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 b28044bc3c..e73719205f 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -8,6 +8,7 @@ from homeassistant.components.unifi.const import ( 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 760e1e4fa4..3a2b37487a 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 @@ from homeassistant.components.unifi.const import ( 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 @@ CONTROLLER_DATA = { 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 @@ CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") 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 e660e57fc6..7ea5e0680b 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -17,6 +17,7 @@ from homeassistant.components.unifi.const import ( 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 @@ CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") 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 ead71db8c2..c00bfca356 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 77efc78562..5339eff055 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -31,6 +31,7 @@ CONF_TRIPLE_PRESS = "remote_button_triple_press" 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 @@ HUE_TAP_REMOTE = { (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 @@ AQARA_SQUARE_SWITCH = { 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 00aa463349..db43c02282 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 6590028d76..4677ea8d5a 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_SENSOR = { } } -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 eb3e5a95c9..dc65008c3f 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -5,6 +5,7 @@ from homeassistant.components.device_automation import ( 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 @@ 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") + 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 2211b30010..c4d2efcb63 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -7,7 +7,7 @@ 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.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 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( { 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 5339eff055..badbe8b865 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 23e320fe15..a7e04f874b 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -4,9 +4,11 @@ import logging 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 @@ TRIGGER_BASE_SCHEMA = vol.Schema( ) 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 ef1b605f4d..7c68be83ba 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -13,7 +13,13 @@ from homeassistant.components.device_automation.const import ( 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 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( { 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 ea37b8e947..9d8ef6bcea 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 f2a82afdc2..5bd5d83e1c 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 ca91cc7051..a65c1acc51 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 9be294d546..22a016e49b 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 27e78507bf..460676a75a 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 331dc3d329..c1ea3c2b76 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -23,7 +23,6 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( 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 d0aeb4f496..2d1bb89d23 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -15,8 +15,9 @@ from typing import Any, Union, TypeVar, Callable, List, Dict, Optional 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 key_dependency(key, dependency): 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 e383f1013a..d9b3df8c01 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -10,7 +10,12 @@ 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_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 @@ class Script: 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 684285a1cf..c500ddca85 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 a0f64eeec7..4a2a2cf45f 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 d842ae39ae..23a8a808f4 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ REQUIRES = [ "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 1982e80dfe..bd611e04c3 100644 --- a/tests/common.py +++ b/tests/common.py @@ -56,6 +56,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM 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 5be354c78f..9bab1ff1f3 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.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 +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 acfa853d59..8a92f69e57 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 9b540c7aa1..a6437ef9ee 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.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 +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 43af9fe3df..31fb6d30f6 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.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 +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 f1d91879f1..60a4f0faa9 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 c500ddca85..29484b671e 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 4a2a2cf45f..2aa04f8ae1 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 bb29540d6d..61d3479d8f 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 874ac6334a..ed94b6913b 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 ab8a20822a..59d9508ac3 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 2aa04f8ae1..cda5afabe7 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 aeaa0a62c4..c8bc76fbe8 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -445,7 +445,7 @@ class AlexaTemperatureSensor(AlexaCapibility): 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 @@ class AlexaThermostatController(AlexaCapibility): 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 357e0e3026..94c931e351 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -9,8 +9,9 @@ from homeassistant.const import ( 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 df9a9c7512..c3b9334691 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 cda5afabe7..4f0963cdbe 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 76f15f3bdb..f99ec2f22c 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 4f0963cdbe..c84b57a09f 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 f0d72af23d..55b5878f66 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 3cafa89902..e5e5b8ab7a 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 3f48e2afde..581ce6b461 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -7,10 +7,10 @@ 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, 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 70b79becb8..1749ea91c5 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 a7e04f874b..fa6deac40b 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 a69ca7ab8f..4abf34e666 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 032c765bf5..5825a3ba91 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 afb8c3934a..df82ba6076 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -8,29 +8,31 @@ from typing import Callable, Container, Optional, Union, cast 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 d9b3df8c01..05b2810272 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 8a92f69e57..fa78ae9441 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -4,10 +4,16 @@ 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.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 c8bc76fbe8..b8bd3841a7 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -326,7 +326,7 @@ class AlexaColorTemperatureController(AlexaCapibility): 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 94c931e351..d53f145e6f 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 32b4a7e2b2..da1e173b4a 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 0000000000..b0ad4660d0 --- /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 0000000000..f1c20e5346 --- /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 0000000000..392dbf35f5 --- /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 1a5d992ef7..448b654c86 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 66c02884a7..4d442a4543 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 9ddf336c06..67a65f752b 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 0000000000..170c2851d0 --- /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 3f9bfab330..512d0285ac 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 0000000000..b63f0d4445 --- /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 0000000000..130a856826 --- /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 3604eb66ae..bf3c059f81 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 b68c7784d0..cf448bb35e 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 78239148a4..60a3eb65ca 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 ad2f19b066..1f4968f24e 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 670bc23ca1..1da4b4b4b4 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 0000000000..2fc98a70ea --- /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 f7a6bfd9c7..a0a9d087d1 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 25f0545c34..705ac59b98 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 3a7223ade2..756eb65d4f 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 460f01e301..a82da0ce24 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 0000000000..ff0383c7f0 --- /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 0000000000..f960785203 --- /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 503530e728..1281784dce 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 dde5165c5c..cea3146a3a 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 a0ce81495a..598bc77aee 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 0000000000..bfe62cc4e7 --- /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 0000000000..02ac8ecf27 --- /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 0000000000..1e1e8897c3 --- /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 0000000000..58c62fb64b --- /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 0000000000..485150d292 --- /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 0047c762a9..edf38b4fd3 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 cd61807259..2f6f42311c 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 9dd4657291..8d88fe9cae 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -40,7 +40,9 @@ TRIGGER_SCHEMA = vol.All( _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 @@ async def async_attach_trigger(hass, config, action, automation_info): 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 c4d2efcb63..89fd9add69 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 7c68be83ba..c9588c1efa 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 0000000000..1074236eed --- /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 0000000000..7df239facd --- /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 bd611e04c3..0684e6daaf 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 async_mock_signal(hass, signal): 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 4677ea8d5a..91714e647b 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -3,7 +3,7 @@ from copy import deepcopy 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 0000000000..1dc41f5bff --- /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 36c0f52f41..5e1bbc76fb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,8 @@ from homeassistant.util import location 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 @@ from tests.common import ( 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 0000000000..c25be28fdb --- /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 2bfebf145d..4e7b0a0cd2 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 c6629982d5..9db7e270e6 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 8b295634c9..71550fc37b 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -34,6 +34,7 @@ from .const import ( 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 @@ CONFIG_SCHEMA = vol.Schema( 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 07882d8dac..38ae09ced9 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -98,7 +98,7 @@ class CloudClient(Interface): 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 @@ class CloudClient(Interface): """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 df1b8ef165..e28d75f017 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -9,6 +9,7 @@ PREF_GOOGLE_SECURE_DEVICES_PIN = "google_secure_devices_pin" 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 @@ PREF_SHOULD_EXPOSE = "should_expose" 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_CLOUDHOOK_CREATE_URL = "cloudhook_create_url" 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 8986f8f399..38e4aec56e 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 @@ from .const import ( 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 @@ class CloudGoogleConfig(AbstractConfig): """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 @@ class CloudGoogleConfig(AbstractConfig): 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 fce530ddce..f243eab8fd 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -7,6 +7,7 @@ import attr 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 @@ from .const import ( 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 b15fa32cb1..c8fa688456 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 d6e78e87e2..a8ff775a22 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -20,6 +20,8 @@ from .const import ( 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 @@ class CloudPreferences: 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 @@ class CloudPreferences: (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 @@ class CloudPreferences: 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 @@ class CloudPreferences: """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 61e0c70b6b..a1252d67ff 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -83,6 +83,13 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): 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 daaf790a0c..207194d79e 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -3,7 +3,8 @@ from asyncio import gather 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 .const import ( ) 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 @@ class AbstractConfig: """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 @@ class AbstractConfig: # 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 @@ class GoogleEntity: "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 d68650fb63..aea226348b 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -25,10 +25,16 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class GoogleConfig(AbstractConfig): @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 d2e016cb5d..f97977a740 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 0000000000..33bb16d783 --- /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 6ab6d937b5..f9b311a388 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 29484b671e..a64e0dc38e 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 c84b57a09f..fd44b46c64 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 61d3479d8f..9bc9870be1 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 c8e84016a2..a7c8898659 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 0000000000..43914f489d --- /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 d5a3395440..8e03fb82b2 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -33,7 +33,9 @@ SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/subscription_info" @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 244c22d248..e160ea8826 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 12de2eaba1..8049ac4b0d 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 1c7e020113..9a8b9643cf 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 0000000000..bd59502a3a --- /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 6a82204a26..6ecd4af446 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 7d8a68a970..b3f99038b5 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 e5c8c6af14..7df2db1b17 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 b65665eb2c..37bdcd82af 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 1074236eed..c8655d91c5 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -5,6 +5,7 @@ import homeassistant.components.automation.numeric_state as numeric_state_automa 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 1dc41f5bff..7118c94c5c 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 @@ from tests.common import ( 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 c25be28fdb..651ee17bd6 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -3,10 +3,24 @@ 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 +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 c8655d91c5..50fb1dd5c1 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 7118c94c5c..45452dc84a 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 2cf463d1cf..cd5d327f2c 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 fd44b46c64..45296e4250 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 | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 581ce6b461..ebbd1771e8 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -18,33 +18,40 @@ from . import CONF_ACTION, CONF_CONDITION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA 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 + 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 + 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 + 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 68d8a88df4..7afdbae5a2 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -50,7 +50,8 @@ class EcobeeBinarySensor(BinarySensorDevice): 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 8cf9af0e3b..24ea3d281b 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -61,7 +61,8 @@ class EcobeeSensor(Entity): 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 4fd3b062a1..e84ed35c76 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 45296e4250..5328ca322c 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 9bc9870be1..f3cacfa888 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 b3f99038b5..68537aff29 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 a189199bfb..7448eb27ae 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -137,14 +137,13 @@ class TahomaCover(TahomaDevice, CoverDevice): 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 b564cff733..a5b879e566 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( 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 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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 @@ class SSLCertificate(Entity): @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 0c11656878..29af1049fa 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -23,7 +23,7 @@ from homeassistant.components.camera.const import DOMAIN 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_setup_platform(hass, config, async_add_entities, discovery_info= 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 4e7b0a0cd2..d2cda1f1d0 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 75aec037a2..1179fd9086 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -36,6 +36,7 @@ CONTENT = "content" 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 setup_platform(hass, config, add_entities, discovery_info=None): 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 setup_platform(hass, config, add_entities, discovery_info=None): 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 setup_platform(hass, config, add_entities, discovery_info=None): 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 setup_platform(hass, config, add_entities, discovery_info=None): ) +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 @@ class TodoistProjectData: # 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 @@ class TodoistProjectData: 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 dbf1a941e0..e7876c953c 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 e69de29bb2..c2d23cc4be 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 5328ca322c..be88d6c60c 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 356c7fe574..a49e4c9c05 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 @@ from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, @@ -543,9 +543,6 @@ class PlexClient(MediaPlayerDevice): @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 @@ class PlexClient(MediaPlayerDevice): | 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 @@ class PlexClient(MediaPlayerDevice): | 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 @@ class PlexClient(MediaPlayerDevice): 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 @@ class PlexClient(MediaPlayerDevice): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE ) @@ -682,49 +679,74 @@ class PlexClient(MediaPlayerDevice): 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 - def _get_tv_media(self, library_name, show_name, season_number, episode_number): + 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_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 @@ class PlexClient(MediaPlayerDevice): ) 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 @@ class PlexClient(MediaPlayerDevice): 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 d4393d38c9..df9e9f9f6c 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 @@ class PlexServer: 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 dd5401950e..38727ccff0 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -12,14 +12,7 @@ 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 ( - 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 @@ from .const import ( # pylint: disable=unused-import 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 .const import ( # pylint: disable=unused-import 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 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - 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 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # 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 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _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 6538d8e887..aff79acc2e 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 753d565a82..e9f48f6a4f 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -6,14 +6,7 @@ import plexapi.exceptions 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 17a6709496..08a0430ee2 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -61,12 +61,15 @@ class WebSocketHandler: 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 @@ class WebSocketHandler: 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 68537aff29..5bd5b7ea76 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 207194d79e..933f0c0799 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -182,6 +182,11 @@ class GoogleEntity: ] 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 33bb16d783..869bc61d7a 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 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig {"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 bd59502a3a..734d9ec7fc 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 df9e9f9f6c..6ab1143076 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -57,7 +57,7 @@ class PlexServer: 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 60a4f0faa9..58e5558781 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 a64e0dc38e..04a68bf963 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 be88d6c60c..7aa5090688 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 f3cacfa888..69c6ded580 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 89fd9add69..f138bcfd5a 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -81,8 +81,8 @@ CONF_SOUND = "sound" 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 @@ TURNED_ON = [ CONF_MOTION, CONF_MOVING, CONF_OCCUPIED, - CONF_OPEN, + CONF_OPENED, CONF_PLUGGED_IN, CONF_POWERED, CONF_PRESENT, @@ -118,7 +118,7 @@ TURNED_OFF = [ 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 @@ ENTITY_TRIGGERS = { {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 @@ ENTITY_TRIGGERS = { {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 @@ ENTITY_TRIGGERS = { {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 109a2b1fd4..e01af8d183 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 0000000000..bf50b4f23e --- /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 0000000000..652cc46a7b --- /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 0000000000..cb290dc46c --- /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 0000000000..83284aaeb7 --- /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 0000000000..0c29ad0bc6 --- /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 0000000000..cf756a9f49 --- /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 0000000000..e50f618575 --- /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 0000000000..08aac57d16 --- /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 0000000000..7e2f4f1ff6 --- /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 0000000000..70924bb7bf --- /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 0000000000..5d601b3759 --- /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 0000000000..36080c9f37 --- /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 0000000000..08f57d88bc --- /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 0000000000..bb38d2b9b8 --- /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 0000000000..7c129cba3a --- /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 d1264010b7..2d7964f18e 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 0000000000..b0ad4660d0 --- /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 3364446935..b6296d1aca 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 67d720aa85..951263d53f 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 6379df936b..93b6189398 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 0000000000..80792f1663 --- /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 6a795dee13..d962c79312 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 0000000000..67d4f85262 --- /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 ce1f1ab3ca..98ab98e6b1 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 97e25e2896..830ae0fd13 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 cb5db0b834..04a08d185b 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 cc6d22945d..3729f7f556 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 7a2b883286..1f0b344a32 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 840bc8929a..f5f41a28a3 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 c7079fd621..7d05a366cf 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 9aebb2a556..0717bcfc39 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 bd47a63776..2ad613cde6 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 0000000000..5a96b853eb --- /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 0000000000..1959f769d3 --- /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 0000000000..85da5b3a4e --- /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 0000000000..301239cf31 --- /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 0000000000..fc349a0d9d --- /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 c7b85c1959..32bf473ac3 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 830391f58f..5e40c8ec5e 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 1405112c07..62d24662ab 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 0000000000..d8afb1e7aa --- /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 7d6583bc1d..d6763d17e2 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 52b8798c0f..6942f683fa 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 e8f4df930d..e98df7466f 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 82ecd4a325..5155a42c4c 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 be5d2b7159..79a46e1861 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 0000000000..ea78f0d0d5 --- /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 0000000000..0e024a0e1e --- /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 a260efa5bd..a302572ed1 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 0000000000..89922b66f0 --- /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 06a5b7e69d..0c3afc88c9 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 0000000000..eaf7601be9 --- /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 0000000000..98345b022f --- /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 1e96214237..d033da4bae 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 0000000000..c78905b09c --- /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 e07adeb0a3..be8966d955 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 0000000000..6e084d1a9d --- /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 498b5b2f12..b569cce923 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 d37aa3567d..7ae83b550e 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 0000000000..2bab2e4300 --- /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 b70d3f12a8..2fd772c861 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 0000000000..0e024a0e1e --- /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 d44142213b..f647dcf7b4 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 d92d28d948..559382cf20 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 0000000000..d30f9e5ad4 --- /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 0000000000..7f0d122f38 --- /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 0000000000..2a75d11a9e --- /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 0000000000..73628c8646 --- /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 0000000000..99e7574e4b --- /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 0000000000..941ed18660 --- /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 0000000000..d5615815dc --- /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 0000000000..3043ec6ec3 --- /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 0000000000..e04e73aef2 --- /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 0000000000..084c4b50e4 --- /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 0000000000..caea115b7d --- /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 0000000000..1a206258e2 --- /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 0000000000..7acbb718d1 --- /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 0000000000..61f49cd5da --- /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 c7e89c368c..7345cf4629 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 0000000000..0224d663a8 --- /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 0000000000..b8abb48af4 --- /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 0000000000..274dd46488 --- /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 0000000000..65d7d9e92b --- /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 0000000000..8ad9d89b07 --- /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 0000000000..82b9a7aee8 --- /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 0000000000..9c62686e19 --- /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 0000000000..ec1f719a6c --- /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 0000000000..4fec1baba7 --- /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 0000000000..3d018a2292 --- /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 0000000000..6104aa7de7 --- /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 0000000000..7e4a0eed01 --- /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 0000000000..718322ec17 --- /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 0000000000..5de551d5d0 --- /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 0000000000..648f156e86 --- /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 9683c5d7c3..58d57b2805 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 0000000000..cdfd651bee --- /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 0000000000..750e14b1da --- /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 11e11ebc6f..a3ba518537 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 0000000000..9508310227 --- /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 efdd75b148..bf927b7f1b 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 6d1ad1f62d..261ca95149 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 812de425ef..c9e61dcf2e 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 3c28f1d25f..8f61f968db 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 1e6488784d..7b0f723297 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 0000000000..a16deb2fca --- /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 a0a9d087d1..18c4e865a8 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 ce9d2e1e88..9b75a0061e 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 2b63840d00..fe773f72be 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 49ed34baf7..9be270a017 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 5f6d0c41c1..2d4ce1ea6a 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 0000000000..865155c049 --- /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 b3302389c8..8692090600 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 9ab6156549..cf842efe9f 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 6eec3ef0eb..6248890389 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 0000000000..59db5a62f8 --- /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 0000000000..df9b9935dc --- /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 0000000000..1f248099df --- /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 0000000000..7bbbe660fe --- /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 0000000000..a9039d2e41 --- /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 0000000000..676a5aa413 --- /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 0000000000..07b20245c1 --- /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 0000000000..01a4e89c9f --- /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 0000000000..5f5eeaacd1 --- /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 0000000000..da1dcc1d6f --- /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 0000000000..e3bc994b6e --- /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 0000000000..af97681ee7 --- /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 0000000000..0568cad3f6 --- /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 f685297890..e82172f92f 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 88ea988ff1..03b17b3ba8 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 fe36e4296f..d8622cdd2c 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 0000000000..d93eec8aed --- /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 0000000000..8126b6ea5a --- /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 0000000000..e990fb98dc --- /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 0000000000..d8aba08253 --- /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 0000000000..6eeb4f75a3 --- /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 0000000000..0ed881853b --- /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 0000000000..3d28389ff9 --- /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 0000000000..ff0383c7f0 --- /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 0000000000..934f56a420 --- /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 afaaf4edbf..9d3c97ad90 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 b8dbeff27c..eed288a5e3 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 0000000000..1d9fb41fc8 --- /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 0000000000..9fc23b3e39 --- /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 0000000000..ed0342b943 --- /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 e1bc8dc322..67461d1a3e 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 0000000000..e2360c016c --- /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 0000000000..7622ac1b45 --- /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 5da2d4f9ef..e7a438cae1 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 0000000000..02ac8ecf27 --- /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 0000000000..86e5d9051b --- /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 3741b035d7..899b532290 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 53b794ed43..0d0315e49c 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 e447e89644..32a378b7c0 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 2025bad624..d9b65b6d1d 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 0539f5607b..1db6712142 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 8c2526f8a1..c40b782207 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 5285ed2187..80b546ebcf 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 05b0ffc0c4..4fa1f62c60 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 0000000000..7c129cba3a --- /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 c21a47c7ea..9041f01842 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 6366f82b3d..5887460a8a 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 76802a9636..d7451bd81a 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 35000bf4e1..7084c4609c 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 498afcbb10..5e0b881af1 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 286efcf035..cfbedd994a 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 8d41ec1d5d..3351f0d5d8 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 0000000000..372e37133b --- /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 0000000000..c1c8830cb2 --- /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 0000000000..7d8b268367 --- /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 ad553def6c..3bdaf85498 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 0b800ecd80..39f254ac9a 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 280c941b42..9ffd5211a1 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 48328aed87..f8b78af572 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 0000000000..ad2c240baf --- /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 390069b769..18c4c3c9ff 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 dc408035d0..6a017e9e1c 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 ebd9d44796..8d1c737170 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 a64b4db185..ed2e20f352 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 5bd5b7ea76..2ded4cde47 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 f2be849cbc..9acca6f0cd 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -4,7 +4,7 @@ import logging 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 create_withings_data_manager( 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 f28a4f59d8..c781e785f5 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -4,7 +4,7 @@ import logging 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 @@ class WithingsFlowHandler(config_entries.ConfigFlow): 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 d38b69f224..ae5cd4bcdd 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 7aa5090688..c9869b2553 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 69c6ded580..03a0c13dfc 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 e35a83bd24..8a33baabc2 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -105,7 +105,6 @@ TEST_REQUIREMENTS = ( "mficlient", "minio", "netdisco", - "nokia", "numpy", "oauth2client", "paho-mqtt", @@ -182,6 +181,7 @@ TEST_REQUIREMENTS = ( "vultr", "wakeonlan", "warrant", + "withings-api", "YesssSMS", "zeroconf", "zigpy-homeassistant", diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index b8406c3971..f3839a1be5 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 @@ NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( ) -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 @@ NOKIA_SLEEP_RESPONSE = nokia_sleep_response( ] ) -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 7cbe3dc1cd..0aa6af0d7c 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ import time 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.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC 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 @@ class WithingsFactoryConfig: 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 @@ class WithingsFactoryConfig: 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 @@ class WithingsFactoryData: 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 @@ class WithingsFactoryData: 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 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: 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 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: 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 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: 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 withings_factory_fixture(request, hass) -> WithingsFactory: 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 a22689f92b..9f2480f909 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.components.withings.common import ( 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 da77910097..697d0a8b86 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.entity_component import async_update_entity 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 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): 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 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): 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 59d9508ac3..9790fbffd0 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 c9869b2553..7a8ce0780b 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 58e5558781..67a66bc961 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 04a68bf963..19acae64b1 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 7a8ce0780b..44c10f8ff2 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 03a0c13dfc..2a8f51cd19 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 2ded4cde47..c5b566e508 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)