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 ad001e56048..0c6ac82894a 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 00000000000..b6edb9be87b --- /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 00000000000..ba44d86c2e4 --- /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 00000000000..3e1ce22a230 --- /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 da36928d6ba..8a4bb391da4 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 4fb1c67a56e..84a6a6e2934 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 8a4bb391da4..ee42e82f971 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 07adcfc79f6..7125d3e9ed5 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 3095d140619..6a2543e8b24 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 ee42e82f971..48fe33d9e58 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 1b595924106..6b74c09107a 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 9b98914314a..0f1277e0b05 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 90b85aaeba5..f14e7b4c667 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 75dcac93dd9..f024386aa0f 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 00000000000..cf449ebb358 --- /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 00d03b12d01..6ab87274409 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 269715b7cc3..8f5fec9b309 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 00000000000..698c28d99bf --- /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 00000000000..eeffc9262e5 --- /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 00000000000..0607f4ab08e --- /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 00000000000..f930df12861 --- /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 419d4b72864..4e253741b05 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 48fe33d9e58..09b8ffb746a 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 63ce5b707cf..7c88e615fa5 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 c9cc9b2d333..c13de455984 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 011908d5458..96d541b1955 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 eec3f678981..8c534a88d30 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 6a2543e8b24..e78661a04e5 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 09b8ffb746a..2075dab9a37 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 e760f91070a..8f01036da75 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 0b7228fb568..a30d08f31f3 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 00000000000..b0ad4660d0f --- /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 00000000000..a3536902c76 --- /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 263730ba583..d36de4acc1e 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 0f1277e0b05..cc6d22945dc 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 f14e7b4c667..7a2b8832864 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 0ddff8557ec..923a2beb2ff 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 506461ea50e..994e13f5674 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 23e98919bb8..92fd1e3e749 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 00000000000..74ae5541754 --- /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 cf449ebb358..97971b99e9f 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 cb4682fc937..947425e4807 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 00000000000..201e3ff49c6 --- /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 00000000000..0948a31739f --- /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 00000000000..0321a6b73ce --- /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 9e567fcc394..8c2526f8a15 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 f930df12861..8d93adbf4a9 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 8943643e8b3..3e2fea2342e 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 8f01036da75..6a26775b0a8 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 8c534a88d30..0c614a9dab2 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 0c614a9dab2..42815d8c8ae 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 35cf63fc3d2..a090dca0d0c 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 d84a47c6aaf..a6138087f1c 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 00000000000..72966ba6c66 --- /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 c17aa0b6639..d84706430f4 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 971d38f9ab2..46f21d96195 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 b6aa1cefb02..873dfee9a92 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 9135ed3b478..73749382dd9 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 8bcf03914ce..3d2b3f17814 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 994e13f5674..70c33cf3c02 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 86210b2e6c1..9aebb2a556f 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 00000000000..7326d80497b --- /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 00000000000..e2a7f94b3d8 --- /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 8fc70a60a05..28999de5e81 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 b56875453dd..93dfc65bbe1 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 68e770e8873..afd59d619e0 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 00000000000..7c0d17c8a8f --- /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 00000000000..9f52511a165 --- /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 00000000000..ebfefe40b0e --- /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 00000000000..6749eab1293 --- /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 00000000000..38edfe5a195 --- /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 ab8c0e70cd4..b0b65a10c83 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 02dcb71f54e..902e28b2080 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 e60ef7b4c67..1acaaa53ab2 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 379f69febbb..27257e5039a 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 0c6ac82894a..fef77c8a247 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 56a39df64c9..dec91186be2 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 00000000000..09c9322a587 --- /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 321c54329a2..f41d17837c2 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 fbfb10783ee..813af7863f1 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 4a1691e0314..81021d0b447 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 f2fc51ce713..8efb473cf54 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 594f4f6c54a..4ac4614379b 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 5eeb14b6359..05853d4b260 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 43812df94d2..770921288b9 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 058e21262e3..ababf793f0c 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 56663c6b2da..af9f619cb3e 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 00000000000..31ba0ff3581 --- /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 2d0a69d7d06..ed9098559a3 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 492b16a603a..b81ecdc5164 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 1844cb2c97c..b7a1ebce22a 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 e6249b2304c..68daee6cf26 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 f6c2d471bbf..31588db1f23 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 a6138087f1c..001721d4f00 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 af9f619cb3e..558b0fe4205 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 12e2e092f67..c63b1721393 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 3f00c31c7e8..d7071d6daef 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 b0456e0b624..d0586565521 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 fef77c8a247..824fb3828f2 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 18218bbf68e..fe5e19f9115 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 2480daf2ead..563fe261093 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 69293ede516..17b11d6aef9 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 73643a5383c..ce1fda0839e 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 2075dab9a37..e5ecd69ee24 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 847635d4dad..fda5c743222 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 08a49c4a667..fb35485f5c9 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 367ea52b3a2..776d8f39f69 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 40fa08856c5..3525f1121c0 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 dc4cb7502c5..8ceda6cbd3e 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 7047e6e8d92..5c8d46cb727 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 7dba7347651..3bd29a72a12 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 c04a30589ed..a9463cb78f4 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 43338c9e14e..0a48388b718 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 f4226ecc630..484c47d1190 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 0625fd4c27f..2ae2006512b 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 2413edaebce..244fda61656 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 e5ecd69ee24..056b51c3457 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 5fa7d49b885..2afa18af32e 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 87d8134ccbf..0b1291d4045 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 d3b0d8dd301..87898e42d59 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 563fe261093..37744dce180 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 31ba0ff3581..3498b46d879 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 d0586565521..7d630498cde 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 00000000000..63934871fcb --- /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 558c0c39f66..13f0915bc56 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 27257e5039a..c2273c54073 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 9997ae00f0a..d7282317d95 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 056b51c3457..049e3a423c2 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 46f21d96195..5886d8e5c5b 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 00000000000..b0ad4660d0f --- /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 00000000000..b0ad4660d0f --- /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 817737eee04..d29481a3be9 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 2cb0bd9af16..b10518646ac 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 c088e414c73..3774956330a 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 a02a6ff4223..f3eead4aae0 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 3d2b3f17814..1bc6c8211a2 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 60a27304d78..41c75ec4aab 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 f9f2d40488f..116f6254b37 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 8798248224a..7a93c6ff9cf 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 88730a18554..70d766cf4c0 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 955b050bc5b..882b67823ba 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 41404822dd8..f6f592675ab 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 416f0878de4..cfe1d347b0c 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 642e76fd1dd..67f6daa8469 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 97efd428a04..ca7bce44508 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 2cad909a7ee..f8ae990d364 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 56e7ed62e9d..3ec9ed871d3 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 00000000000..5b37bde3ee3 --- /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 7326d80497b..698be68bd78 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 00000000000..4beb11214bc --- /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 00000000000..9d464a6d516 --- /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 28999de5e81..2b185cb1b6c 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 00000000000..533ba76b6a7 --- /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 93dfc65bbe1..fcbe835e4c2 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 00000000000..fdd76cda7e6 --- /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 39c391eff33..2241ca6644e 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 afd59d619e0..432c8ae37d1 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 00000000000..6eeb898ee1f --- /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 7c0d17c8a8f..511f3c9d8e5 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 00000000000..d3800238559 --- /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 00000000000..c43f434562c --- /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 7659ab4d296..a475518bd85 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 660f639d859..9f91d37c233 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 e52a80be986..ecb061e91c9 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 33b6b1d3827..9a94e54dd5f 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 17757cb9d20..0986b0e0240 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 00000000000..72f1ad2a4c7 --- /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 9f52511a165..8708729bf4a 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 00000000000..957a0187c1d --- /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 00000000000..ad7cb55316b --- /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 00000000000..31e41d3f504 --- /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 6749eab1293..987d13a3940 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 00000000000..291d8cb478f --- /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 00000000000..8a00ac09549 --- /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 b0313a1eee3..677e0389d45 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 b0b65a10c83..dedaf02971c 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 902e28b2080..60a412684f7 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 8a8d8b11f57..3741b035d7a 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 8b0eb562037..0539f5607b4 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 1acaaa53ab2..1e1e8897c30 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 99611c5f9bf..856dc77a52c 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 00000000000..7d1dbad19f3 --- /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 00000000000..994d02aa7f5 --- /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 f604594bfc5..d5d35ba7c9f 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 e67b8f70160..8db6fd4609e 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 7052ebfc15a..955c4b90cc6 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 d635d178940..b01efba8f04 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 049e3a423c2..f59f0ba19ab 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 7125d3e9ed5..297ed6bb866 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 fe5e19f9115..c454514912c 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 2e15ac8a68d..c8b19082585 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 dc0ae1f2249..7fda26b2900 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 932b80690c4..e7f078a1a05 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 53bd7fc5820..efa38286e7e 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 f59f0ba19ab..0e432bff8bb 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 955c4b90cc6..01823882f9d 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 b01efba8f04..43a22cb980c 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 0e432bff8bb..c2be62c2325 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 297ed6bb866..b00194b0d91 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 7c88e615fa5..29e68a5d7ac 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 63bdbf8a928..5c5a095c8f4 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 c2be62c2325..80b79563e85 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 4c21bb7fdef..51f615e68aa 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 2cc46632dda..13784e24d77 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 8bd1952a650..7c13488c3f5 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 d12ecd9d3a6..45f603a2cb4 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 fda89158293..f0e9b06b762 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 03eedd6d162..9e08a9cff1f 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 7ca71fc4f93..17efdba3fb5 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 a668c78598a..40bfc4ca0a1 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 722b92e33f2..1593e70771a 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 460114b143c..77b842ba078 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 857b3763076..77b842ba078 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 a01dad03d46..b05c04a16f1 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 3525f1121c0..27b8b860d72 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 3bd29a72a12..1ebe4785761 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 873dfee9a92..51bd522f2c4 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 ab68d5ba08b..7c7efea7333 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 dd3463fff95..d73762ce882 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 fccfb295c0f..b564cff7338 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 f8c99496a56..f44e65512e3 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 23cc74281e8..f28a4f59d80 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 88b8e6d5ea0..1a99abc7255 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 93b9a434b7f..3ae9d11c3b6 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 01823882f9d..3d5860d0a43 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 43a22cb980c..de5c823d999 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 80b79563e85..05a3e89b675 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 b00194b0d91..07fc31ec6ef 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 51bd522f2c4..873dfee9a92 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 15e50a40127..7f0566ddd42 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 5017af8e576..4cdf3d9042c 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 60ccbd99348..225d64be231 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 fcbe835e4c2..533c4b3c0f3 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 6ab87274409..f0aabef2ae7 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 03baf0c032e..991222d45be 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 c97565ddfe6..6fea704f756 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 5be333cbf13..ed036023755 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 987d13a3940..b38928fa753 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 161b5d85599..28ad9ce954d 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 2b906dd8003..16ce491e776 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 3dfe0265bde..a5a7f320d93 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 b82144d37c7..bcd408c25a7 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 ec1dfd2bcb1..bf4b05089a8 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 b1fd4b10f46..1b51256580a 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 2de70f6d247..246c2bae7c5 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 ecce762f51c..50a5b2adaca 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 6b691bcab8e..c574ed8911e 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 3d5860d0a43..628099a47d2 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 de5c823d999..bbeb228f345 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 05a3e89b675..415c8e514f5 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 07fc31ec6ef..5ff52cf2255 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 0635a2d1f11..4fdd513f840 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 171f25128c6..b47cdb98161 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 001721d4f00..cc3f3de3170 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 b6745e1a971..c5c35f10829 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 b76b3511a09..1211188d3db 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 eb391cc563d..947c42e6949 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 0806ba0799c..10161856a47 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 628099a47d2..978127c6342 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 bbeb228f345..5eeec405e7d 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 415c8e514f5..9848716d895 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 5ff52cf2255..69ca7eefe03 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 129579315a2..a4300e1e530 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 00000000000..d6811728a22 --- /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 41c75ec4aab..c536e577141 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 c03ad0f7e1f..1e962142373 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 4cdf3d9042c..c9b727088ab 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 7b266ba7412..4ea4a94014e 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 fcfc2773ed8..2fe1c6b42dc 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 225d64be231..3f37de5331e 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 533c4b3c0f3..6bf91651d2e 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 f0aabef2ae7..fd30e931718 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 85a117f0b53..2f4d2ca121f 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 7277ef5900f..e055f67421e 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 fdd76cda7e6..a7f807e8dcd 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 546fea78b6d..63954ca83a9 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 2241ca6644e..008123739d9 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 9debeaf4169..22a93909578 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 3154e17a509..ba9339c1a94 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 432c8ae37d1..bef4f1583b6 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 8f5fec9b309..5ac06129463 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 d3800238559..cd3c7152c89 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 1e9c089828f..40c7c8853da 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 1c24acd96e4..ac88ed224ed 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 2a10b234e99..48751096948 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 957a0187c1d..afc558ca80c 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 31e41d3f504..efccc652d5b 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 6fea704f756..dbf5e152656 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 ed036023755..391a071cb8f 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 b38928fa753..24dbc2cdc1f 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 eeffc9262e5..4775d62bce3 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 c51ce8c6ee5..254c09380c1 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 2156ea04e01..02c303f9329 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 291d8cb478f..8e974a0a8de 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 1d8355d2158..5e2aa6747a4 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 8a00ac09549..adc128991c5 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 f564d1424ea..c63799bf783 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 1b0658cd174..45a941b665d 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 38edfe5a195..89423e071fd 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 0607f4ab08e..c1a67897b16 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 012aa65187c..0eddbe2a151 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 00000000000..8808d85a1d6 --- /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 00000000000..0b07c5003ef --- /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 f907364327c..518f0066534 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 00000000000..89e0bd818d2 --- /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 7d1dbad19f3..cfccd8b1dbb 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 a96f8cff523..2f2fdbe9b3f 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 3c2f00ba4ae..617964e0596 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 994d02aa7f5..9015f490830 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 1729879a154..3776621bec2 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 d9d5e14208f..c6c621fbdf3 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 160c8a5e455..8e67da86dc3 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 33444f10996..ed22be003ad 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 68f0d77e307..e86ec02040e 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 10643f134d7..1a406d743b7 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 b81a2193bb5..2253f261ad2 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 c7c488950cc..5dd7ab7a88a 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 eaae3f19236..457c319d9e1 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 643e006dfef..7a0dfa82e76 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 ac156e040d7..4bc79e7bd39 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 83e4c089b9a..ee74b369629 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 d16ed23266a..1e6a0517026 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 5805114252e..697be4d1579 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 cbbecb1a403..413d2a70aae 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 f2e0ccac2b0..cdd6af57617 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 446a36ec350..ad2072baaa5 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 e5746e088f8..b721fa29cdd 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 1385d5e59a7..90b4b386f37 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 1ba2c4809b6..82db5f6725f 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 9e313fd3694..4ec40731c5d 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 d769f797da1..aeaa0a62c4b 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 708d1592e4c..0b5c1243764 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 1e636b96ee5..c72101460c4 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 bfe459b0cfb..0d96492e3aa 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 69ca7eefe03..87f02e53b79 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 5eeec405e7d..0a5cfbfc67d 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 9848716d895..a00441518a5 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 5ab8d74c64c..8be458fc449 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 5724d7a3bac..fce0129a7bf 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 4e1ffce7e22..9749ab9bb71 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 98fc70f3bf5..ce13ca5a594 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 ba6b21e0028..d847c6df24f 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 a00441518a5..99e94ec438a 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 87f02e53b79..0ac705bf2e1 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 69fe3bae618..4c691f66af8 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 0d96492e3aa..44b27d8e13e 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 0ac705bf2e1..83ec2d1d2c1 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 52e50ab4799..3a154b857fe 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 0c1cfcf21da..a5b4547b344 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 99e94ec438a..e6ac06e9199 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 b6bad10d608..e7706b0435c 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 e6ac06e9199..e67a7a00c79 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 0a5cfbfc67d..746485f2ece 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 e67a7a00c79..8cbf9230ec0 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 8be458fc449..e6776d8a1a0 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 824fb3828f2..0e4199bb097 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 827e05a424b..15fcfc15338 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 69e77c8854f..665091d69b9 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 00000000000..3c683c802f5 --- /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 6f19623c809..e77ac303bf1 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 00000000000..11c15404f45 --- /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 4269400dc24..94d990952a6 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 cfc63948bee..bc19ff41dfe 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 f469e95da80..7d5b54356a0 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 962e074996f..f41a9bdabae 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 00000000000..396a3387fee --- /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 7f3f5c1f20d..9ddae5acdb9 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 83ec2d1d2c1..ef8618f146b 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 ff2943a583b..72fb9ff5a44 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 00000000000..9c9c00d87ac --- /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 00000000000..d0270878280 --- /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 00000000000..9c9c1b62525 --- /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 610ec92a2b3..83d70830b11 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 8cbf9230ec0..99d81158edb 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 ef8618f146b..3d0d42cc847 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 0e4199bb097..5d5b0f6c81b 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 c454514912c..19dd0d5c8b6 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 00000000000..5293ad2a1fe --- /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 00000000000..7f80fb077cf --- /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 00000000000..c932c66627b --- /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 00000000000..eb57a36a2bb --- /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 00000000000..4da7bc9e4af --- /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 00000000000..3630c28605b --- /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 00000000000..2f6747ab4cc --- /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 00000000000..7cb14b03c6c --- /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 9ddae5acdb9..9a534c01bbf 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 99d81158edb..ae4007ee770 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 3d0d42cc847..f9644058580 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 72fb9ff5a44..384d50bccef 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 00000000000..1baeb3fee82 --- /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 00000000000..faa920271e3 --- /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 20652ddd046..feda5da732c 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 ae4007ee770..064762591db 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 f9644058580..2f5a1bef6e4 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 19dd0d5c8b6..9766f011889 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 fd74d9831fc..131d1a23b6a 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 c2834fbc788..83805692e4d 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 4ff59ac1770..fb38b41f314 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 609ea72cc69..99ca3cb1ccf 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 8c231ec63e0..a08c0a59c07 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 5d5b0f6c81b..a29586c7b6e 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 9766f011889..9bcd475d5d4 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 00000000000..d007b2d3df8 --- /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 00000000000..54837b2eb09 --- /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 00000000000..340291807ea --- /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 064762591db..437aa60cf96 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 00000000000..fe6b0db52d3 --- /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 00000000000..50d945e7fae --- /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 00000000000..c5e4857297a --- /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 a090dca0d0c..75898b0fdab 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 8d27d386da2..a84e799d44d 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 c5c35f10829..2f42193291c 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 1211188d3db..cee91f00c42 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 246c2bae7c5..5c7ee48a78a 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 72966ba6c66..ade9aa02ad4 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 d84706430f4..25a1cd465c5 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 50a5b2adaca..14dc5cc8eac 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 074e943548d..dcc8ba500c3 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 947c42e6949..928e527dd07 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 63934871fcb..533d85eef7c 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 c574ed8911e..262bd7001f5 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 978127c6342..18c19a9012a 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 746485f2ece..900bfddda2e 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 437aa60cf96..29897b56227 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 2f5a1bef6e4..122ff317c0e 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 00000000000..5498624a061 --- /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 00000000000..2ada5e810ec --- /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 00000000000..2e77b4ba976 --- /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 254c09380c1..ec742e4113b 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 c63799bf783..921048286b6 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 89423e071fd..f1b851b05b6 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 c1a67897b16..517d48354dc 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 5bf342836ce..51276869ec6 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 1643ecb1480..3c345a1a788 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 d0fcb6a5276..71934516ea7 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 30a77102d04..9e408eb0d5c 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 a29586c7b6e..5d5b0f6c81b 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 9bcd475d5d4..9766f011889 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 d007b2d3df8..00000000000 --- 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 54837b2eb09..00000000000 --- 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 340291807ea..00000000000 --- 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 29897b56227..ac49c415398 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 fe6b0db52d3..00000000000 --- 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 50d945e7fae..00000000000 --- 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 c5e4857297a..00000000000 --- 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 153f6b5aea6..dffb4b52667 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 ac49c415398..522f06bca02 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 9e08a9cff1f..f0529f126f1 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 d491765bb00..8d2b4430fe1 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 7298ce8c1d0..8ef662ec878 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 d8fa8853c7f..7d1e24f3698 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 4eba4657d95..dac08afe471 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 2027a8fc458..8f3b5d87f8c 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 e53954a65dd..952fa41c42c 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 0b569e2d4ad..23728b65109 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 98e3849bfb6..9af1998e894 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 0a9bac30188..00f5984c58b 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 5d5b0f6c81b..a29586c7b6e 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 9766f011889..9bcd475d5d4 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 00000000000..d007b2d3df8 --- /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 00000000000..6c633f848c0 --- /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 00000000000..340291807ea --- /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 522f06bca02..f59ed13bda3 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 00000000000..fe6b0db52d3 --- /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 00000000000..50d945e7fae --- /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 00000000000..c5e4857297a --- /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 be079e83fa6..ff9f27d4843 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 cd407cfc416..25c11a9fd4f 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 c35cb168fdf..ac83c2cdcd8 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 ffd5aa21472..d279af46335 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 6d13f7adbfd..189b9365105 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 f59ed13bda3..e66f248cbba 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 122ff317c0e..c254da1f9d2 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 8a03ac47402..cf26955b207 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 e66f248cbba..9d7e8b645b1 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 c254da1f9d2..fdc9ad8dc3f 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 00000000000..2eca398d998 --- /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 00000000000..d1b514ea934 --- /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 00000000000..cf66bb4e2ae --- /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 00000000000..d99cbe8026a --- /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 00000000000..352d1da206c --- /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 00000000000..f7b3f56f2e6 --- /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 00000000000..83fe922d8c4 --- /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 00000000000..356c7857d92 --- /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 00000000000..c05141ff0b0 --- /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 00000000000..e8a1c494d49 --- /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 00000000000..a99a32bb950 --- /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 00000000000..7c1e141eef0 --- /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 00000000000..0f29967b286 --- /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 00000000000..081b6d86600 --- /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 00000000000..7735f497f80 --- /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 e58c901f364..f8f64d54260 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 d6811728a22..9620526e363 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 e095cc360a0..73e899106c1 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 00000000000..07affc990a8 --- /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 c536e577141..1a03143f11e 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 92fd1e3e749..612c5afd033 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 00000000000..2499befecbb --- /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 00000000000..3786b03f41f --- /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 8d064622f7e..d9fdc8f91c2 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 7a6e9a8a915..b528cdeb70f 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 00000000000..b80d9bee4e2 --- /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 00000000000..c90416b0619 --- /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 00000000000..69b8ce8a31e --- /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 00000000000..c6e075683ad --- /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 00000000000..fcd5c1df019 --- /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 00000000000..4f90cf71cbc --- /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 00000000000..7e632c8dd62 --- /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 bfed5937e24..3af9ab00728 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 00000000000..b450a3cbdb0 --- /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 6ebaa08457f..9a3ef350ab1 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 9565bb66618..9027347174d 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 f43fbeb58b7..c7e89c368c1 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 00000000000..8d5c25babfa --- /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 00000000000..eb4f6459f4d --- /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 00000000000..58a5169ac02 --- /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 00000000000..d2610c68aed --- /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 00000000000..130cf2067ab --- /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 00000000000..8ac90efe3d1 --- /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 00000000000..606f97d6965 --- /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 00000000000..46cd613df4a --- /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 4775d62bce3..807b85c5fb5 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 adc128991c5..3469079f230 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 921048286b6..199b150f68e 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 45a941b665d..b769e56c974 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 00000000000..248e8f9f44e --- /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 0b07c5003ef..b6f10842b4d 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 00000000000..80301cfd57b --- /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 3bef273b83e..05b0ffc0c44 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 89e0bd818d2..f38a74e5c1f 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 00000000000..7b2bc3b028b --- /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 00000000000..caa00f36c89 --- /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 b66786cc9e0..ad715d54eb1 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 9015f490830..5ca969f0391 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 22d8884d66a..d32c9640fd7 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 00000000000..c7485b09248 --- /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 ea3e801ac53..ebb0895bd7a 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 9d7e8b645b1..18ca0565846 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 fdc9ad8dc3f..621863c8efd 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 b0e5fdb2088..bad90d9e827 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 18ca0565846..ef036c99c70 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 621863c8efd..a8aa92c81dd 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 436d25750fc..0e450f06238 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 9bcd475d5d4..700e7145838 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 5fb07011983..291c774c383 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 8586d950e39..4fc62e44921 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 ef036c99c70..58c59a1c04c 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 4617fcf054a..bcf398464bc 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 af27ff8c7d1..28f1a7e9720 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 900bfddda2e..32804d79041 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 58c59a1c04c..8af0da567da 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 e6776d8a1a0..87704a3c6a9 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 dec3e78a503..6040c8655b9 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 313a6337a11..09cf40f193f 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 8af0da567da..c67928093c3 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 ba44d86c2e4..850eae76040 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 3e1ce22a230..75c1bd3dcd3 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 c67928093c3..a17c201a952 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 700e7145838..abd3379221a 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 2d19ab25fe7..7fe8bba6873 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 607d9c45538..ea5a64d97e7 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 02a6eccc285..58c417831a9 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 e455d5581d1..1205521706e 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 d63d1707fac..2df50699a9d 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 a17c201a952..b4ea5b509d3 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 a8aa92c81dd..214f2ee30ed 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 32804d79041..842cf4840c8 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 b4ea5b509d3..42082b76ae0 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 87704a3c6a9..26f112bb008 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 44b27d8e13e..e697164a35a 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 214f2ee30ed..e3df0099032 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 e35be24fc80..0547628b4bf 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 42082b76ae0..1e2c466a754 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 7ecc298e3f6..be7f0a524dc 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 1e2c466a754..4baf0de152e 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 e697164a35a..b9b919c4bfd 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 e3df0099032..6b4d7fbc089 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 bece55ae09d..451a6f3e33d 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 4baf0de152e..08ed0d4476f 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 eb006f408bc..1ffbe69888f 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 ac334587b89..f16758a5355 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 08ed0d4476f..ad7ba89e4ca 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 a1a35e9f3b1..44cd95e1f5d 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 00000000000..c609c2eb5da --- /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 00000000000..109a2b1fd45 --- /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 00000000000..91124d47f4e --- /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 00000000000..5052b8e47f1 --- /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 bf136731cb6..012c1e1df34 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 8a6a37762d4..cce2307bc4b 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 ad7ba89e4ca..939fcc27978 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 a29586c7b6e..302ff946554 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 87b073db052..bca91134bed 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 00000000000..3dea978044f --- /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 97fdfd9d36d..615899a98c8 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 627a9882154..4877dbbb541 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 4be72eb7359..545c1ad93ce 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 665091d69b9..dd458dda078 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 e77ac303bf1..478dd3754e7 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 bc19ff41dfe..4d097253ea1 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 302ff946554..65ee6e9e259 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 abd3379221a..640a9a7bcc0 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 00000000000..8c61ad54184 --- /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 00000000000..4dfe04f9c2e --- /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 00000000000..0c2d6d93661 --- /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 00000000000..7e23edb1259 --- /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 00000000000..926f73fa4db --- /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 00000000000..4ff6435b64d --- /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 939fcc27978..250643f745d 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 3c683c802f5..e620e4869e5 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 396a3387fee..c093d4fe0ce 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 9c9c1b62525..e98aed793cf 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 65ee6e9e259..a4f52af76ce 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 640a9a7bcc0..8fe47035912 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 00000000000..860c7d4dcb4 --- /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 00000000000..42b58e7f50d --- /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 00000000000..066f3270ccd --- /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 00000000000..2a2f50532b4 --- /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 00000000000..5f4f2defe32 --- /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 250643f745d..614362578e0 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 567d0b95ff3..7bb124ae543 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 00000000000..6379df936b8 --- /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 00000000000..5a1916bce59 --- /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 7a93c6ff9cf..3968c1f00c5 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 612c5afd033..558fd9e5897 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 00000000000..7448100158e --- /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 008123739d9..785e9ca2912 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 ba9339c1a94..a6a7994b7c3 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 00e3337dfe1..8e9b3272f94 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 2ada5e810ec..7fa9f62be07 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 00000000000..c79a49470e0 --- /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 58b6e1e63fd..c87c1a702c8 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 b769e56c974..cd5cbc0d6a1 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 d311b3b0d38..090de517036 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 37dcd8ac22f..a033d2954d9 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 068f4341544..c21a47c7ea2 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 a1614eb1ce3..21c911a66ce 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 d1b514ea934..93bcc5aba41 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 00000000000..54a182be31b --- /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 d99cbe8026a..75a869572fd 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 352d1da206c..a7263daaf41 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 f7b3f56f2e6..6bccf6529fe 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 83fe922d8c4..68ab771122e 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 c05141ff0b0..e08851f47a0 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 7735f497f80..35d8a96ab2b 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 00000000000..16d13aaa99f --- /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 356c7857d92..00000000000 --- 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 a99a32bb950..00000000000 --- 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 00000000000..7ab8b736782 --- /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 7c1e141eef0..cb4ecac61fb 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 0f29967b286..00000000000 --- 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 081b6d86600..00000000000 --- 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 a4f52af76ce..38e39177348 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 690d3102f9c..00000000000 --- 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 98db00c3094..00000000000 --- 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 cfe35a9a63f..00000000000 --- 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 614362578e0..0ca24c8e9d4 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 38e39177348..06177f069c0 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 61da78fa6d7..00000000000 --- 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 3141314b049..00000000000 --- 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 9e2f8886d3a..00000000000 --- 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 7e26e6c9e5c..00000000000 --- 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 0ca24c8e9d4..0a6f1979a91 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 06177f069c0..b6bdcc27043 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 8fe47035912..ae072cd092c 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 5243fe379a7..00000000000 --- 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 c1453d88d81..00000000000 --- 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 b7c94933a39..00000000000 --- 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 0a6f1979a91..a3c1548bc1a 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 ae072cd092c..7e05cdf0b39 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 00000000000..9a5c8ec32ac --- /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 00000000000..e26e2e1d6ea --- /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 00000000000..ba4908fe85c --- /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 a3c1548bc1a..bf217688d26 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 6b4d7fbc089..9e846c9c416 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 384d50bccef..d74a57d678d 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 00000000000..ac0ec709654 --- /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 00000000000..783209690a3 --- /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 00000000000..9b682f6c51f --- /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 00000000000..a3af39129d0 --- /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 00000000000..08da738f046 --- /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 00000000000..bda8454f3f3 --- /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 00000000000..765c438c1cd --- /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 00000000000..07881e8bd3d --- /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 00000000000..149b4d06c39 --- /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 00000000000..52df0d4eb35 --- /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 00000000000..81fb246178c --- /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 00000000000..a776fa91c43 --- /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 00000000000..a302d564902 --- /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 b6bdcc27043..256a6b3e41e 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 71e04d7b8c9..00000000000 --- 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 050a78223c1..00000000000 --- 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 f1d1787b7b4..00000000000 --- 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 bf217688d26..011e8b7a096 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 9e846c9c416..c0e94a5afe5 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 2a278cf1d38..00000000000 --- 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 e33902c3fd8..00000000000 --- 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 256a6b3e41e..9fe3e10c8bc 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 d685ab50372..00000000000 --- 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 b34a8b8383e..00000000000 --- 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 2f499e52e23..00000000000 --- 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 011e8b7a096..3214c5e43ab 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 1a2f642f91c..935ebb9b497 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 c2273c54073..fb388afac0f 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 2b167d52ac6..3101abc5264 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 a6a754fc2a6..01c5c12efb7 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 d6d901ac3b1..4e417fca219 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 cea246af328..112fca24194 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 3214c5e43ab..07dd2c39651 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 c0e94a5afe5..7ece1a030aa 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 9fe3e10c8bc..f8c632f7053 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 5898aa36e98..00000000000 --- 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 d40de718f90..00000000000 --- 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 31fafe17edd..00000000000 --- 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 07dd2c39651..c8f95e29703 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 f8c632f7053..88fdbd45f9a 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 be09312f693..d2f842956da 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 75ef8cce19d..d6483902602 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 e78661a04e5..7744b9f2233 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 c8f95e29703..c9b28ae1a14 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 7ece1a030aa..333c644ebf6 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 7ba7d94d251..47f81787acd 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 b4c313041f3..6a7638d9f86 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 a8c55890acf..3fe5e7937c8 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 3101abc5264..08c6cfe18cf 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 49a60a0f760..7381b557310 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 dc3ea35229f..faa44f34927 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 07d5bd8ca7c..ac6bc73b809 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 4fdd513f840..0c116568780 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 2a041ce6689..b83358822b9 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 c9b28ae1a14..62218b14780 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 333c644ebf6..b824054a93a 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 896867fcb17..605c7f9d7e3 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 605c7f9d7e3..e50989a15df 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 842cf4840c8..47325a6c930 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 62218b14780..04b6eaeb3e9 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 b824054a93a..b962ca1280b 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 7bb124ae543..e84de4ffc22 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 00000000000..434c236418b --- /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 00000000000..438822a97f5 --- /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 00000000000..7d73cb8d4aa --- /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 00000000000..9dc3d88322c --- /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 03c128f636c..c68f49509ba 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 eb4f6459f4d..4c24dddbe87 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 00000000000..ea680a638eb --- /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 8ac90efe3d1..b58cdfe728e 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 00000000000..537bd5e3fac --- /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 46cd613df4a..b906d0d8dc6 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 fe93407b429..b539e315ea3 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 5ff04c5ee70..489e66c2b12 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 96d541b1955..28a4e853f2c 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 93bcc5aba41..22cdee8f69e 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 54a182be31b..801b8ebb5fd 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 00000000000..3449009818b --- /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 00000000000..ff15625ad7c --- /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 49f738cf969..4c9c892b93f 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 850eae76040..3eec85b3e53 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 b1c167a4175..e9621fe6bbe 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 e675d18a00b..f3a7121c0b4 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 b977a087eae..65e20f558a7 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 9419cbaaefb..279ac3b103c 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 04b6eaeb3e9..037fb1fcd2a 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 b962ca1280b..63ad27a654c 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 d74a57d678d..649c48e1b7d 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 88fdbd45f9a..a4d6d0d201e 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 13ade253ff6..b6097737414 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 4c9ed56352e..04d38441621 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 c6258e244e9..362424c7fac 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 037fb1fcd2a..b8d4a158655 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 00000000000..e6cf69341f9 --- /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 b77528e0c32..a8768193736 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 9223399bee7..b3813da1766 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 f0da251f5eb..6a819fbc16f 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 aed12bc65a5..3d4a03fb0ac 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 1db4aafeeb9..82d20ff78c2 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 00000000000..6a96ce5aa3e --- /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 e1ed6a678e3..cfc32a020c6 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 00000000000..9de04ae8e66 --- /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 5492582ebed..67724e9fcf3 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 a4b9bc5cd76..af9375aad05 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 727ec91dc37..324dcb019b3 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 6da26784d9c..3c753d832e0 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 023fb32e6e4..9ec8c56d770 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 9d34cc6fb79..b36e0a34fa4 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 7d2f51f29af..75aec037a25 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 1da70bc60ec..913d193845f 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 108fcbc7740..9a1f375fdfd 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 5a5ffb34ab1..80642a373da 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 223ce810d7c..841b283a98d 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 dbfb9ae1864..4c1a9803d75 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 00f5984c58b..ecac61895c5 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 c29d41ace9a..31761f2560f 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 af8d5589c8a..4911c5d5fb9 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 4911c5d5fb9..fad02dee075 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 f0529f126f1..f669d415854 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 b090484ab67..fe2d65edef6 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 d372aedd1d7..26dacac974d 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 3f2aa1c00d7..0ef0884d329 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 bd1da7e7e1f..e4eb029d5aa 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 7bc4c937765..9512db8261d 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 fd9a778dbfc..135a421f72e 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 b33d724d770..9dd4657291d 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 5fbe97185a7..184b9ea302b 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 7cbbe56f326..66892784a54 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 c83d660912c..f2b4134de42 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 3942d0efadb..231bc346e14 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 f749a308bf7..ee092916112 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 706afbe9042..bbcf9bd9ddc 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 35b11006024..535ef298a2a 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 c609c2eb5da..00000000000 --- 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 00000000000..70b79becb8b --- /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 00000000000..2211b300104 --- /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 28f36b8f431..77efc78562a 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 9508dd9c849..b444abd5238 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 1593e70771a..b7cadd1349a 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 00000000000..ea37b8e9470 --- /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 61292d47449..00000000000 --- 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 00000000000..a69ca7ab8f2 --- /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 00000000000..f2a82afdc2d --- /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 00000000000..ca91cc70512 --- /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 61292d47449..00000000000 --- 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 00000000000..032c765bf59 --- /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 00000000000..9be294d5460 --- /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 6a96ce5aa3e..46e3beafcae 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 133251e779d..afb8c3934a7 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 952fa41c42c..113f2437ce8 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 23728b65109..14ff873d4d1 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 fda5c743222..bc39b1f5e0b 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 91124d47f4e..00000000000 --- 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 00000000000..b5502d8fe3d --- /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 00000000000..5be354c78fc --- /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 0be566d4b52..6590028d766 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 00000000000..bb50778db52 --- /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 27b8b860d72..00000000000 --- 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 00000000000..8009fbd6337 --- /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 00000000000..9b540c7aa15 --- /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 00000000000..888e06e0214 --- /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 1ebe4785761..00000000000 --- 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 00000000000..e2ce5a373d2 --- /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 00000000000..43af9fe3df3 --- /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 9de04ae8e66..5a4b9d5616e 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 9b8c4ab283f..d62dede5abd 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 37f792cec45..63e9956d0df 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 550a0ce1d13..3809249bea6 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 56a87ce4345..994c3e2fd89 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 07028de0d42..77426e8ae2c 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 0f3ae7da710..05170a54ed1 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 37fd18efb6d..efc0bfbc822 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 d18ff9fc46c..60428961cb9 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 42d35b60b25..8fa3897b735 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 8b26a958a6f..6f1c66d8d87 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 9ec8c56d770..af92f6c5f05 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 6806df0408f..86d26ec25b4 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 ef86ea6734c..461b3a199d7 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 af661f4571c..6a64226a053 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 9e62e7ee0db..6540fca1405 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 70301f203f8..a387b3fc0bb 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 60d49573833..e7f87074cb4 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 f14ea5af02c..8c83de202a4 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 01a3338e540..20aae3849ab 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 5af784359d8..d0bb27c06e1 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 d888fb82528..07d3ff4becc 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 7c1979f3ab9..31cbc0c65b6 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 fd18772edbf..a28f53f93d4 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 e69564453fa..dd9e3833801 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 ccc55691ee1..9822b7c63c2 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 ecf904ac9c9..74c8a99b51f 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 00000000000..6b4e144d9a6 --- /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 00000000000..cb2fe821297 --- /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 a440fdde4f2..3998494606f 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 d2610c68aed..171c656566d 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 b906d0d8dc6..b424be059f9 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 00000000000..2a03b7d0e8c --- /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 6a819fbc16f..84b335bdeaa 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 9db55494ba4..f639c85c682 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 b9b919c4bfd..ae4401178b1 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 63ad27a654c..c92b16b78fe 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 7744b9f2233..f46904f3267 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 b8d4a158655..3a40af2b1af 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 f46904f3267..c4de1d66e83 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 3a40af2b1af..2b9fc631994 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 7f70b0cfac0..0606778acb5 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 afbb55e03f3..53245176a04 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 ecef388c0d4..c49319abf42 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 340291807ea..26311a4c72e 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 26cb3c20942..9aa4544f5c3 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 f7e24d69884..b416b1f98d3 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 c77a90c1f8b..2d4dda032ca 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 9a07a935d12..3adc5dcad46 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 42d8d89a021..417616161e5 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 2b9fc631994..2f8ae35125d 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 6e2298e05b9..26d0f351fa2 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 b3f29e93c63..5ad29af0aaf 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 26d0f351fa2..7a337dcc497 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 558b0fe4205..0ea91d10b19 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 c63b1721393..488d48bb740 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 62879a82724..ad23a564272 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 fe8f49d260a..4aec29008de 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 2f8ae35125d..73ececb0517 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 c92b16b78fe..6f542147363 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 d7071d6daef..4d8d3a31258 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 25a1cd465c5..b98681b6fc9 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 7d630498cde..986e01a1599 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 1d266d23d3f..54abd54caaf 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 2afa18af32e..7d6e79a8237 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 ccb74e88e37..12de2eaba1c 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 6a7b69daabb..6473e8964b8 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 0288aa87572..a5c527dacfe 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 e7706b0435c..dd4df479af4 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 4eb3881e95b..fbf4fffb17f 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 73ececb0517..26fdfc38fa3 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 488d48bb740..66df687047f 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 4d8d3a31258..d0423c394a6 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 a4d6d0d201e..a8932f54a54 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 7e05cdf0b39..cb9180d717d 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 4e9a4a3a4f4..f10e1b4bd69 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 00000000000..9e7e9fed396 --- /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 cb8b7436b51..eb65a7ed426 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 a3cd49ff458..8b7b819cfc7 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 181f1561eba..9eb8e8f26bc 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 00000000000..f4cd4fc5bf0 --- /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 00000000000..c3a23099b8a --- /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 31cca1e676f..092594c41fc 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 bb6861a1492..c7b3f47d29c 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 d21f937dd20..e62d68dc9bc 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 e69de29bb2d..87eefed9eaa 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 00000000000..9e7e9fed396 --- /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 b09e06bd822..dd3112b636e 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 9a534c01bbf..b6865f9e86a 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 26fdfc38fa3..5ca7a7c8160 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 6f542147363..d6c0d8dbbb2 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 649c48e1b7d..fcb265bbc97 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 d6c40ddf9ab..90a9a641776 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 00000000000..7b4d1f96a37 --- /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 96c749a3413..5a3379f71ce 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 c0c7c462f90..cb37dd3036f 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 19b4a1333b6..456678edac7 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 5ca7a7c8160..6481137f3e6 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 d6c0d8dbbb2..d42120c339e 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 1c891b9c840..582cc185bc8 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 b854b62853c..1b146e9cb12 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 f534f96b780..dc053e60de1 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 bb764e375f3..6c02e430ba4 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 3936aefab0c..8f0d0906311 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 df429460541..c624aaafa34 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 00000000000..b0ad4660d0f --- /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 00000000000..9b9741b9601 --- /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 00000000000..56822c2365c --- /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 00000000000..0b10e1f51a5 --- /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 00000000000..139cff2187f --- /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 00000000000..7c82ef8b9ba --- /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 f3eead4aae0..c9963e49623 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 1a03143f11e..840bc8929a7 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 00000000000..bd8503fabd8 --- /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 9e7e9fed396..39072f70d82 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 00000000000..48d6eacda91 --- /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 00000000000..26a55aa4ed8 --- /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 4fae0249fd0..02354204f24 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 533ba76b6a7..33b57d9e7cd 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 22a93909578..f8f4a2761d4 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 00000000000..fb77e5da8cb --- /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 58a5169ac02..812de425ef4 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 130cf2067ab..244044b2f67 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 72f1ad2a4c7..e4223e373fd 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 efccc652d5b..64a3ea94e1b 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 199b150f68e..31187aaa1b7 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 e4aa0d8c088..0de9452b3cd 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 00000000000..7fe89d491c9 --- /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 00000000000..df36ab070d7 --- /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 d8f571c968e..df5654ff78b 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 00000000000..e769f83d28e --- /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 00000000000..a12436936e6 --- /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 00000000000..e75860d0e16 --- /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 642a2c0af13..2715ef46dc8 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 3140648f57a..8d99b6eebc9 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 f3e7053ca11..49a754f1da5 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 56c2518f11e..5e5c666b1a4 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 f639c85c682..36bdfb6d5d3 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 cb9180d717d..6abb7535574 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 f18f5d4f8dd..536848d3aef 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 dd4df479af4..68045ff0584 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 fbf4fffb17f..4644875ee8b 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 6481137f3e6..5bbd77f4d01 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 dd458dda078..874ac6334ac 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 e620e4869e5..cf70b7470cd 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 4d097253ea1..356c7fe5741 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 f41a9bdabae..09274472915 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 c093d4fe0ce..812e7b81a7c 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 e98aed793cf..37cf0fa200c 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 a8932f54a54..d42d7cbb3b3 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 6abb7535574..11b99b42a44 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 00000000000..7160cd109c4 --- /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 e7f9b94046d..e6ddd87bdf5 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 00000000000..99376f4b6e0 --- /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 00000000000..e4a8b1490c2 --- /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 00000000000..b5f74f7bf40 --- /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 bc5da64fcac..2bd4571ef93 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 ac2e64ce92f..30dfa4a3cbe 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 e049f89b3c6..ab383584e83 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 00000000000..7160cd109c4 --- /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 df490cdbe47..0bb43f715ac 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 b6865f9e86a..ab7b339e582 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 d42120c339e..d790a423de3 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 fcb265bbc97..1e484e0dfc4 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 00000000000..b8f8d8c847f --- /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 00000000000..e79f5c8ac96 --- /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 11b99b42a44..419bc1a8606 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 1d8ed8e37b1..0b51be1f258 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 00000000000..51c5f7a9dbe --- /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 584ea22afe2..b911aaa57e1 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 2d4549a21d5..501566de682 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 00000000000..cdac7171a25 --- /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 868e8ff4c7d..cb4ac83090f 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 5bbd77f4d01..3b9eb718737 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 9eb8e8f26bc..460bd2bb4a4 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 092594c41fc..131c35d7f89 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 87eefed9eaa..2155d3cf7d2 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 3b9eb718737..19bdc4efd83 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 d790a423de3..3a4fa60f15e 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 0f854db0be6..813405cec62 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 94535d7e945..2cd6cd72f6d 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 5c8d75d4cc8..f1ca796363d 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 054b1a89ae8..f446bf7390f 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 be337bd3f0e..3b21726bcbe 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 a6ce441749d..88be279ae7a 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 7ba95b007c9..675c5e18776 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 a4300e1e530..5a816bce140 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 cae2e940d56..c5d84f030fa 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 28859cbf591..1539429d0ef 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 7e3ed3ef888..6c7c88a8045 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 dd1d6f54377..5603c14fe1a 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 1457db2b600..c0d0df02135 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 00000000000..36c72dcb9e6 --- /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 d36c929e721..6b8166f23c0 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 d5383fb1a2b..711ac320397 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 1699bcad8f0..457b7d1b89c 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 0f4bdf98ac1..42b14833aa0 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 1bc6c8211a2..cb5db0b8348 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 3968c1f00c5..c7079fd6219 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 f024386aa0f..bd47a637761 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 21da9b4823b..db41ee98e01 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 e9eabeff638..0be75b94be9 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 e27d59a40e3..4d23ac8aaba 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 18a476b6870..b6bd30f7997 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 18d3d92e16b..3cb54145ad8 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 00000000000..7a42a9470db --- /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 00000000000..2bf141f6489 --- /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 00000000000..660e0064bb6 --- /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 00000000000..f4a63bb449d --- /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 00000000000..e1eb6ebd357 --- /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 70d766cf4c0..be8033f316a 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 4409616cef4..1956c453a9f 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 bec33c26d10..003a3db8bf1 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 836b5c8bc68..488a09c3768 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 c9d98da1afc..c21e76a6eee 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 dd4cb48a090..d41051b6674 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 c45f9c467e4..9efacaf163f 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 7340569e64f..68e87e9aea8 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 f95ccf57272..c6a960f1a68 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 7a08b44f25a..6bbe75a8019 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 481ab372e91..2fe38659fad 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 a125b96a070..798ae93d33b 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 fcd5c1df019..9068b18c82d 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 63080a30ff1..ae32d43f1b6 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 4c66f0d0133..908394bcfd8 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 c9b727088ab..b8b3bbb8125 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 4ea4a94014e..14a747f6eff 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 2fe1c6b42dc..e07adeb0a36 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 00000000000..7d7e158f3cb --- /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 63954ca83a9..e742a337cca 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 f8f4a2761d4..17c81c471f5 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 de62d2ac2f7..4fdef0a987e 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 8e9b3272f94..c5ad3043004 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 62bb6bb9d96..7dd598c8fc2 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 c68f49509ba..23b951bfa62 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 b9f82b6e2e5..1eb3b71c942 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 91c616b69af..3d1cbd41a34 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 4b9ab3a7abb..f16533312fa 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 b3f1e4740b9..99a760dce4a 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 03cf1a82b81..743c47e00c8 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 af89dd3d39b..f672f519f40 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 9f86cd12cc4..5838dcad30b 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 d8c195cb277..923f452450b 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 4b47f52eef9..0de31a35eb2 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 20cdb405f4e..1bf211476d8 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 ea680a638eb..670bc23ca1f 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 7fa9f62be07..d3c4c0d5a78 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 b58cdfe728e..393639dd4c9 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 b424be059f9..c547e4306b4 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 00000000000..702cec128c0 --- /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 c79a49470e0..2cf3fa2c1a7 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 9f688b2e5f9..f1bbb1c872c 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 6d152108117..2053d2f4a80 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 f0a71b4be5b..a786b0c74d3 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 c38afd989d2..7f80900d828 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 6b53e943304..f7f7f4a1d8e 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 c837abad499..ae47916ac85 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 520a29b7602..c6fb13c3605 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 4946eb81f02..41148475a5f 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 d44faf195e5..250a5509c4c 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 c835ddf76b2..dccd39b6997 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 dea146b649a..805b952690e 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 f5402454294..85e8994dc55 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 00000000000..3a619d8154a --- /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 7160cd109c4..e1bc8dc3228 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 00000000000..f6ddce2a4a7 --- /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 00000000000..5da2d4f9ef9 --- /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 00000000000..30004af17db --- /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 0d28b094340..c3d6ff16e2d 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 2e85ef7b2de..858970539cd 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 2d5bd9027ac..498afcbb10a 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 813509121e3..fb1508a1aab 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 2a036a1d2f3..1611dac2721 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 917eb0ef3a9..25a4172f00c 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 71934516ea7..7f32ade8a08 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 9e408eb0d5c..f01109b2d62 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 36bdfb6d5d3..623c33637e0 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 818d041b4f1..2762adc0fba 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 e31e42bfbcf..bbfb3fe7128 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 f70eaa48260..1d5584a82a0 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 6643faa85bd..c085addad4d 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 d68f47b1b0a..fcf4950f5e2 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 19bdc4efd83..a9ec02ffcc6 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 3a4fa60f15e..c3f40e16349 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 86d1c1c15bd..73aa5225989 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 39b392c97ee..feffc70d841 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 7587d0cd42d..db79d81736e 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 419bc1a8606..4a6dfdbf6e6 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 0d80ce770ce..5baf928352d 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 d7ca22b2be3..e837cc4bbbc 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 b7ff9d17fe8..42c16919a45 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 2b3f9f34adf..c05eed2a89b 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 edc51dcc533..b89947343c9 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 a9ec02ffcc6..32983063775 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 dc4b0bd33ae..eec7108cdea 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 29e68a5d7ac..31908166dda 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 31908166dda..626c056cef8 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 f4cd4fc5bf0..56ce13f7701 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 626c056cef8..29e68a5d7ac 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 4dfe04f9c2e..70699de394c 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 0c2d6d93661..81e28438d56 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 00000000000..04b764e271c --- /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 fe2d65edef6..eb3e5a95c9c 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 6d4b465fceb..569d1de6a02 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 17efdba3fb5..97ddf1e0714 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 b444abd5238..62d338ece54 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 d3bd97dad8f..0e840e1d003 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 113f2437ce8..d567962e328 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 1a9a3d256ac..272271eefb3 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 b05c04a16f1..6dcd8391bf8 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 9e5ea15293a..5228f0d4882 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 bd4f37bd135..18143c088be 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 29e68a5d7ac..51c5cdb936d 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 84b335bdeaa..ea1ad48bbff 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 37bc6c7a2c1..b07658e72d0 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 00000000000..27e78507bfb --- /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 46e3beafcae..37ec14bc433 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 cfc32a020c6..a41f6de24be 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 00000000000..6e7bc6ab4b1 --- /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 5a4b9d5616e..2f4ddb6b8b2 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 dbec1484508..e2254073290 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 240809548ee..0820feb8d0f 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 f18a66d9a5b..cec93f5af4a 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 03d153f5927..f0d72af23d5 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 bd5a4d25edd..3cafa899024 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 37ec14bc433..331dc3d3296 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 22cdee8f69e..2258840f430 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 801b8ebb5fd..47cb9709c3d 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 00000000000..d5674f01b2d --- /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 00000000000..f8a00bf1ec8 --- /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 00000000000..d19fa8817a0 --- /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 00000000000..d9cef083510 --- /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 00000000000..f7e9fc091f8 --- /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 00000000000..c22197bb136 --- /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 01c5c12efb7..b14f5fedc22 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 4e417fca219..9829044a53e 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 952ca473fdc..e6a70c9f643 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 31761f2560f..e011db33c34 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 d567962e328..d0aeb4f4968 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 db1c34d8fd4..881534b5bed 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 271b9caa62a..d43658d1584 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 7c61a8ab1e9..f81c40a52bb 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 236e2fc1aa2..79cb2607b10 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 ae4401178b1..7e5be09a28b 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 c3f40e16349..a860c67dbd1 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 47cb9709c3d..ab87799d6b2 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 fc96f2d8c96..c11eb18acca 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 50c8277302f..ce7e53b77a5 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 861957e6ef0..1fb77ce6cb9 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 a85c3a43992..41fc286d13b 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 886d6841ebb..2e7c4f4f179 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 c43fe461a8e..ccd635015de 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 00000000000..27d7acfc83b --- /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 75efdfe3e5d..1447f5483a4 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 1b009582c1a..c60a9ec01d1 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 32983063775..a68f7f71e9c 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 525a4c8e72c..0e9e030128e 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 d43658d1584..6920e0d97f6 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 59f6e4d1c67..621950965f6 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 00000000000..02443d449c5 --- /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 139cff2187f..7862644c727 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 00000000000..1982dd40840 --- /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 00000000000..5c51d86fee4 --- /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 244044b2f67..1e6488784d4 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 393639dd4c9..f7a6bfd9c7f 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 606f97d6965..ea1db4ec2f3 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 00000000000..6cc611fcb71 --- /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 00000000000..9b45e310767 --- /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 8d99b6eebc9..0b800ecd80a 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 ea1ad48bbff..d8e8955a935 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 3a95b90db99..6f103449868 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 a68f7f71e9c..bd0a11f3739 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 460bd2bb4a4..6eccdccf8c6 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 00000000000..3acc3e5676d --- /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 00000000000..ee02f2a33aa --- /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 00000000000..473cdfff1e0 --- /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 103a9fce31e..c7b5535d03c 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 28c02080f16..1c1eed0e89d 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 bd0a11f3739..53f72801cc3 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 a860c67dbd1..7ac46e96cd4 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 3d11cdedc67..5cc204ccc4d 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 4e253741b05..71e1a81135a 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 53f72801cc3..ab99704f965 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 51b9a523b3b..483ee1ae872 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 247ee25027c..d3be01cbdc3 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 e0fc8677200..d5ade703c97 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 c8cec168d6e..143811da209 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 7fd8c4d9b3c..42790e618d9 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 b80522b37e2..5753684795b 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 320dcd2e22f..552c21f170d 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 87fd8cd4db3..c2dd49a76fb 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 5374247dacc..6a6523514c4 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 9e3c535f136..da0e8e59ede 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 05853d4b260..1273278189d 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 23973efb07b..abba183d339 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 b83358822b9..2075f88ded2 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 ab99704f965..9b814ef8edd 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 7ac46e96cd4..9599d24b20a 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 00000000000..34b601027c2 --- /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 b0c8051e69f..41dd3c00d2b 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 00000000000..e53d918f98d --- /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 00000000000..0583a4d4f74 --- /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 7862644c727..059800a116f 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 5bf8db46841..9e810910743 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 70c33cf3c02..d92f318f61f 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 00000000000..2ecb587f19e --- /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 17c81c471f5..4b649744ed9 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 3b34d8f6354..dcbc40b4c71 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 2e77b4ba976..3c28f1d25f9 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 ea1db4ec2f3..ce9d2e1e88d 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 c4d616600f5..ad8a15d06b7 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 00000000000..c3ea3190694 --- /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 31187aaa1b7..201a77a76a5 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 00000000000..17a03b6dba1 --- /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 e4b87c9d7b6..bb05977fd09 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 93867c0c84f..76f1c58fe7c 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 7e1ae1ecd60..7be31d56c08 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 c11eb18acca..3301097bab7 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 2e7c4f4f179..d9fae3fe54b 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 27d7acfc83b..6513d76ca89 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 9b814ef8edd..79313b48e61 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 7da2fcee380..31b895b4eb2 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 184b9ea302b..154394075a0 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 62d338ece54..23e320fe153 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 b7cadd1349a..ef1b605f4d6 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 8ef662ec878..e46423c8271 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 75b45471982..204fcab0381 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 faa4ddfc87d..c5200082f2f 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 0b1291d4045..e77c858fc02 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 3d3c644fea9..2ffb7fea049 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 791dacb7024..98da19fd98e 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 6c49784eded..6b9c7c44ddf 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 3e6048e1532..7d883e273e5 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 57ab34a6a5a..1ec758ebd4d 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 2d748f5cc6a..716b20f4ca4 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 deb3600574f..9d46238b241 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 3886d6c21d0..41232b097d1 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 ba65d5e19a8..025131643e8 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 9a1f375fdfd..17a6709496a 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 65291bc55e7..c8c760a6549 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 1ae76b56252..20a6a90860b 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 2ee03c08189..6ae62be3eb9 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 05ba28e4ca7..d23fb5a4757 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 ccd8e55a4ce..f084492bd34 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 e011db33c34..f4be3b66323 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 3b128646219..0bc27498f76 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 fad02dee075..836ad954ae0 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 1fa0ec76a67..dc48d825348 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 f3866f40e57..08413ecd23c 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 7e5be09a28b..9da375b33c8 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 9599d24b20a..2701513a6de 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 d4b0075aa65..d4222f1d2eb 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 00000000000..b0ad4660d0f --- /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 434c236418b..de7d837b12c 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 00000000000..8e2d326d9d3 --- /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 059800a116f..a7f0bd516a0 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 00000000000..d26f0f9470e --- /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 d92f318f61f..11a1beb10d6 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 1d6a849f3a8..c106543e158 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 00000000000..2c4d16b5787 --- /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 00000000000..5544d2e7f7b --- /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 00000000000..d70be59afb5 --- /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 00000000000..9f82b1a7b1f --- /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 4b649744ed9..33a38fc930e 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 4c24dddbe87..14607868907 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 00000000000..6d1ad1f62da --- /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 2a03b7d0e8c..49ed34baf76 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 2cf3fa2c1a7..5f6d0c41c13 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 201a77a76a5..09b43f4100d 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 00000000000..9fc23b3e394 --- /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 00000000000..395f6e2d681 --- /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 00000000000..f210eb42f8a --- /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 00000000000..a2d3d177e22 --- /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 00000000000..122c332f429 --- /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 00000000000..479e25c6d8a --- /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 a7eac61bab3..ea13035e09b 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 fac325a7097..ee0cf523588 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 635d0ecbde2..2b8230ad689 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 0047c762a9d..b8529ce9047 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 76f1c58fe7c..0e1b7028dbb 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 8606a04e197..0bc3afe28ec 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 30df6716f97..226bd37200e 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 bbfb3fe7128..d7f421c7e84 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 c4de1d66e83..f0f6389a061 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 79313b48e61..dadf60e166b 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 ffc9827eed4..00bb0e2d673 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 ba83bf1d805..54220547950 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 00000000000..b16ed21a5d3 --- /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 e95381cdf73..5296f346626 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 038787ac8d4..1607000e8d9 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 004086ab5c8..39a45429cb1 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 cccb9d25644..3918244d4e8 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 8b5f461c8af..c26ba27a29a 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 e19bbf42aea..772b5dab183 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 2449a1223cc..70423611705 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 dadf60e166b..c25ca6a54fe 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 e78a8e6851c..afb273331aa 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 d42d7cbb3b3..f28e9aaeda6 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 4a6dfdbf6e6..db0ff3226c3 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 00000000000..738d0fd6422 --- /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 00000000000..5bf51e743e9 --- /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 00000000000..e2f89273520 --- /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 00000000000..815a0176e7e --- /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 00000000000..1577b7f2911 --- /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 00000000000..35a77c063b8 --- /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 00000000000..eac817ce119 --- /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 ab7b339e582..21f57934e95 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 c25ca6a54fe..5482af01cc2 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 2701513a6de..801c09f322d 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 1e484e0dfc4..9991a6bc1f0 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 00000000000..8d84668e5ea --- /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 00000000000..764a18d1b8b --- /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 51c5cdb936d..60eff866676 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 131c35d7f89..148e355a3d9 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 5482af01cc2..f6f78bab918 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 801c09f322d..faf775ac5b8 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 00bb0e2d673..95351083b5a 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 be7f0a524dc..fa704a6550a 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 f6f78bab918..54a9ec2d438 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 d26f0f9470e..06534314e00 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 d066cbcc510..8d54c470846 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 1982dd40840..ee1fd5246c0 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 00000000000..65394faba17 --- /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 00000000000..7e3ee3b24da --- /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 ca4cee896b3..5181c37969a 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 00000000000..05414b1e03c --- /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 00000000000..23f519353b4 --- /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 00000000000..9a759e309c2 --- /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 00000000000..6bd4737d6fc --- /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 00000000000..460f01e301f --- /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 738d0fd6422..5dea73fcc22 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 00000000000..ce8e950dacc --- /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 00000000000..5ab3af0ecf8 --- /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 00000000000..7dd523f366c --- /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 9fc23b3e394..4fa0c4e6714 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 a2d3d177e22..cabbb6d9149 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 00000000000..73735dda1d9 --- /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 ea13035e09b..113eaa000fe 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 00000000000..cb2031dc7e5 --- /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 49a754f1da5..a289e05e667 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 623c33637e0..95550ca0999 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 0bc3afe28ec..7ccc661dd28 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 4644875ee8b..89bfee7d4ee 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 c84a4152582..77f3c64752e 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 54a9ec2d438..561e3345417 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 faf775ac5b8..e7e4ed37e00 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 b3982e7327d..ad04b8a0eb3 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 969c2a734d3..760e1e4fa4c 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 ba7a72024ed..14bf1223953 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 0264f76f38f..e5c8c6af14b 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 a0480d62a10..444671cf82a 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 1b37bc3b2b5..b65665eb2c9 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 f28e9aaeda6..aa8f2d8c03d 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 db0ff3226c3..f5cd03882c5 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 00000000000..03277bba4df --- /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 00000000000..c0367c47902 --- /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 00000000000..fa06b2b9125 --- /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 561e3345417..ef1b56222b5 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 d7f501987f9..aa8cc8b1224 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 24cde24051a..d1d30207375 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 8cba3032f54..29eecdfd077 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 307142ed989..01d2fb948ed 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 204fcab0381..39574a2b03b 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 8d83cd0cc2b..9b25a6ef6e4 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 53a4f620dcc..b1ce8ad7ac0 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 f4be3b66323..feb4445d36d 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 7d5debd484d..5c59dc6c13e 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 14ff873d4d1..a4f6afa1636 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 f29d1885d1e..e177c86c65c 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 7f9692b3380..2f49a566a32 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 58e4fc19eb0..07de3b2942d 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 6920e0d97f6..64bedfe2501 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 79cb2607b10..99e606d2866 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 bc39b1f5e0b..1982e80dfe9 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 09793b4303e..17bcaadb92b 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 75a151d7868..d7b7496573b 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 e6f05cc2be0..7a97de0f68e 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 a12b9af0ebd..4a2e4fed6c5 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 ced2f512b49..81430cff349 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 fd6c0f73303..67d826f576b 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 32d73c70d45..b3dcddfba6a 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 e81ce7a4a5a..5ac13027f28 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 023c42cc817..8dede61869c 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 cf70b7470cd..dd5401950e9 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 478dd3754e7..0b436c4e208 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 94d990952a6..137619b27b0 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 09274472915..d4393d38c97 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 812e7b81a7c..6538d8e887e 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 ef1b56222b5..ee439d45592 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 e7e4ed37e00..949da3ad402 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 9991a6bc1f0..e35a83bd24d 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 d0270878280..87fb6df5971 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 37cf0fa200c..753d565a82b 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 47325a6c930..684285a1cf1 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 ee439d45592..4c963ddab23 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 26f112bb008..d842ae39ae1 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 21483756213..692e5526085 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 4c963ddab23..dd38be3bfbd 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 37744dce180..ae3ce7c5944 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 00000000000..84e4af15b5d --- /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 ba4908fe85c..8fd4b4fe94a 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 137619b27b0..8e068c909c5 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 8b7b819cfc7..68d8a88df47 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 6eccdccf8c6..f930282ba7b 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 e62d68dc9bc..8cf9af0e3b4 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 dd3112b636e..6175405638e 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 d37422d4177..fab028560bb 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 02443d449c5..3c12eabe8ff 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 14607868907..11e11ebc6fe 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 d3c4c0d5a78..efdd75b1481 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 c547e4306b4..2b63840d001 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 00000000000..53146bebf83 --- /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 00000000000..1ea53b778ea --- /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 090de517036..3258cf2ddca 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 95550ca0999..390069b7698 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 04b764e271c..3f48e2afde6 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 a4f6afa1636..e383f1013ab 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 6dcd8391bf8..acfa853d596 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 c085addad4d..dbd76a2bc9b 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 dd38be3bfbd..385f2d0e1a2 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 cf2d6d25c4a..23b62bb0331 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 8365ca1a765..b2f1581e76f 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 385f2d0e1a2..245f91dc9c6 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 19140aac939..20d443f225e 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 c7098867ee8..64b8b71457c 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 d377ca7dca0..7ac91964a98 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 245f91dc9c6..35ee05977d8 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 dbd76a2bc9b..dc2d31c2358 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 35ee05977d8..2cf2c765e8b 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 949da3ad402..bb29540d6d9 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 49e0c46fd55..793c19cc466 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 4b8d6967491..9f712ba5c7e 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 e233f430cfc..ddb4954794c 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 0063f1ec370..f207e6dff09 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 0c759f0ad60..cf3e621fd07 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 b9ee8939dc4..a7e8aeb8deb 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 5bfe85547ff..17fa14a9cfa 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 ddb109a99b0..e7ea23a43a1 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 0681d5df38b..7d440407427 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 95e26de53bc..04ef58769da 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 3e0d4112d27..5ab69d94cf2 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 9d2c0a2056e..fd5c3010ab6 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 2e27beb48e6..06269390753 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 e4fc9eb8680..c6629982d53 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 dacc428ea2e..9ac8d1ea1e0 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 20d443f225e..45e382647f8 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 8e5ddb924ca..3d175165abd 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 056930edfc7..8f363ba219f 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 f79ce34897b..4453687b895 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 d20b10b2d15..6bf79e27f85 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 28909f7e053..c9602d75751 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 dc2d31c2358..4fd3b062a10 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 17802918cd2..d4055a4068f 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 9b2e3c697bb..7c648d37b10 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 ac36af7fa48..fb2bd643525 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 813176728f2..08cac545249 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 25d9a76036e..830fc04445a 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 9a310a096a5..4845c45a963 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 c391fb0e14b..f8fd2e0efa2 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 fbe13ca8578..c7615817b50 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 40f1805d83a..2e5dded4af6 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 16865905ae9..d4c491cb70d 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 59ab3c03d92..288b8fb3890 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 cf21cbe87ea..3567ce71cd1 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 d5bcf92a39d..ee6b915e658 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 35803d0d4f6..8779e051dc0 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 597975619e6..ccc4f119092 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 1c861aa67e2..d84202cbac8 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 db1308b0483..56018ba777b 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 bafe43c480f..cf793328d93 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 f36819f133d..3fcdcf42ab1 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 621faba4fc0..55739cad8cc 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 e41491c4b0a..ebaa564736f 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 56ba3fe9356..204327043f9 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 56325dd40af..f49421ea954 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 10be545f5e1..1b0ab33f381 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 9743835af20..63cf0da0f46 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 935cc7a9175..79a6877692e 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 273cefcbcfa..4fb9ab9f420 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 e7d97f13313..8bb8b56cb9d 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 dfa5bec3c00..f4e632cb7d2 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 a473a23f917..a4543cc4b0f 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 2b1bef9081e..348f6148386 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 e2223fc97c3..0b705bddc29 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 1dea1b7e37b..756a1c5adcc 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 25480ac8bdc..7060dbd396b 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 5632836bfbb..edd60326828 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 54cd9a3af64..15a648167c5 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 3e9ad732b74..bc69efb6490 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 90e62c78356..63816f967e7 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 d627351958d..ea29314eb1b 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 85da99a6885..1ffc34fcd9d 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 98cbbc9be56..63c0494c2f1 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 9e3e41290ea..c5b3a632c16 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 98c609731c6..a38ba0bd613 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 a5277c97d99..0a6e2540790 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 c11583ed59e..629bdebf27e 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 8a2a9f7b71f..773b52e724b 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 3a780507dd5..49da6534bae 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 7731f845005..e64e3e61f16 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 d2f8f10290e..30ed924a9dc 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 c853bc5a838..20fe51c561b 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 2342c8418eb..9d01b301d43 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 976be85ca94..c062d14f8c9 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 0cc875c50f9..29366715d23 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 eb1f1d8ca94..b2e7eb08ef6 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 52e8e1bec76..e49e45865c4 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 45ed2003026..d77c32966b1 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 d3b0657fed8..f3dd46c96fc 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 61823564fe9..2905bfcfe96 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 a47e3f69d5c..6ee4344b946 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 927d9ea9412..bee9cefce17 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 725541082e7..985f3012422 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 d25a2526a5d..cc3c3b02981 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 55cd555d989..896ace7ba6a 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 3a09cd090a5..3e6ee8422b4 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 3af6a15ca52..a3395965e4f 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 346c1c99f6d..f76d521853d 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 84a6a6e2934..b6776a17f7c 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 781f27afb5f..97f72f2ad11 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 152c7d3a2dc..c6ef7f8f127 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 9a12ba252e3..4a04ffa32e6 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 abdd2400311..b4bc2ff86d9 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 21c4efe071c..3560b1e7fcd 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 926925a7bf1..8058088bf8a 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 ea1ceaa9531..f46c7ba708f 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 4d835ed4e7c..35368dd6cdf 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 ffd550eebee..a10da6e1cc0 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 38319825094..6a28b3c30ca 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 c2a86f426e4..8aa3eacf405 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 ca5312e7670..5933eaf9071 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 3daeac43da9..b15fa32cb13 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 7716ae65c4e..78bc6de99c8 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 1528f4252b1..fe5b8e155c2 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 ac42e374fdd..f07813b3db5 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 5f8a189c7d1..2da323f0815 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 0afb1b1c28f..ec9aec6c654 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 47c7931a0e9..89fbb84e82d 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 03319aeffa8..57daba7fdbd 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 ff94522210d..4d7dfc8994f 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 f26da49d3f1..f5ff021b6d1 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 9c0c50a2595..c6e99b43c49 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 f01fe7324fa..10c067d4a22 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 ddd3b6205ef..0d6d67cf254 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 9489dc72689..69ab8ee3c4b 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 ae7066ea82d..3fd533054d8 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 da5a644334c..1d82dcb5b0c 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 5a1bdbf5a45..b2abc40dca2 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 9034cb7740d..7950cad9b8b 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 0f74216b9b2..c5cc45d3192 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 def2846c4ca..e30d64510ff 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 7064590bf25..2db35dead60 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 a60355efa0b..f81cb171580 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 a210b5a78d1..189b685d4ce 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 e4e6482484c..0046e51463e 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 40a2e82d53a..36de2ff2101 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 3c877a34841..874b24e370b 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 4aec29008de..1e5cd414425 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 923a543e827..5142b5fb2e2 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 42ab6bfd6c1..14b8829fae8 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 6969d9bba7e..a240599c420 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 90e1a4e3b15..2d550a0851f 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 d33a140cedb..b2eb3ada65f 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 4f167ecae25..a4802c3b67b 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 2068b72fa9d..92b2aebab40 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 34699d666ad..9e084c78e21 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 463c7d03fbb..9a2bf666016 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 a95e9c4f68f..50b1f9d357a 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 40ab85bc1e5..3bea097b831 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 7b32f7845a6..0e1e0e8cd81 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 05889bdd326..e1d9273b797 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 aa8b584aeca..d9e821c82fd 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 2ef940f60bd..eb19a5c3a85 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 990b39b21a5..4c58e090a95 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 7dbe6122ac1..8b65d7a680b 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 ca304bce88b..18282f07781 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 31940c28c4e..50c03bad25d 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 845e1af15d4..56d968189cf 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 c2ede62ee5b..431afc080f4 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 388017f78bb..2f764ae2817 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 8f7d07eb0db..b0b8c60121a 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 bf05d5c7f63..008a1293e41 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 544ac9b0fba..4f3d84da476 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 f8d13b49f93..933af93a77a 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 75c1bd3dcd3..e0dcb48527f 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 3fb9fdc753b..c9cdb32e18a 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 122d774c268..ac0044c7a89 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 514509c004d..241dadddf4e 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 21c98d56d1d..250adab263b 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 fbf7a00f8e6..f3ccbdeebb2 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 fc13fddc936..3e377f3a2ea 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 ed38d35346f..9e0ad6c001c 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 602dfec801f..131063ad81f 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 35e6c4a2449..47250b32cbc 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 a2b21a9e0bf..a35fcbcdf68 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 e0a00620210..75d8cfb6054 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 7b956dd96c8..92940c8c1e1 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 16b033df8fd..d32206da726 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 46b8fb761dc..b9be062d3e2 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 5bd488e0ff4..c1bf938dc34 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 148e355a3d9..bc87b3cd64e 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 3ae6b1eac35..7ce52c021a1 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 4495cb3c2f9..5de2390dd30 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 4684655aa37..36918fa5ee5 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 9fe0e4c50c9..6d1d444d2f4 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 967f04228a8..d4ae0c9d6df 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 f4ca116a647..99b966c6c50 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 6f103449868..b0dfc63d929 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 2b008c3c370..3353f1fa4d9 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 98d94fd009e..0bc242509c3 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 466f9da7e90..75acab5860d 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 04d38441621..b4871a805d2 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 87688733e59..12dbb152206 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 90623c01d1b..83833d4f79b 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 0cb09e3fb73..80d946f4868 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 75fcbc4c555..9b3b00d20b2 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 ba68ce94951..824e5bef7c8 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 d523bd72b72..870681fd4a5 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 e6f1c5d7826..4dffbabd219 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 86d2d69cf9b..5b5f94f7c8c 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 b2b60cff95a..b0910f16536 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 2ae2006512b..c62e1e356b6 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 c69a66d43f8..ddf69d0d417 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 b34aa08951c..6c5405c75ea 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 3fed307aed5..7509e627621 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 e6623b83013..22055e347af 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 21f76c3a31f..d73b331d5f0 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 6d13c79bcec..1065b94c12a 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 43987cce2c9..bde64762121 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 aeb3b48311e..914c8f1556f 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 452d1c4c475..f0abf8c7de0 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 ec7f1fe7072..92a91976500 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 9c2e1b2ae4f..53e2dbf2cb4 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 32a57cf20b1..5633880be35 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 9632906a25a..930047065c5 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 4a3eefc135c..2c911eb04ef 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 fc60658a3f2..6ff0c7be0e4 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 48a73dfb030..e8aa3ab51b3 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 85bb982d2d1..0df3b7a850e 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 f4bf021380c..3655ce22ba7 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 e458d30073e..6ecc9efffb8 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 4a3695e7dcc..438891eca92 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 e9a0e7b1014..5b445dd3094 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 71600b31117..1bb8e7353dc 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 3574e6254de..0a5d1316561 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 343a21ff072..638505d5dd9 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 581b0e14156..07a9cde900f 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 f76bcd27466..14e0a6a487c 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 28f061d26f7..f28007ba552 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 e3580676290..1d0879c291a 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 6a6316d80a3..f550cb75c5d 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 1e010bb06ed..e6661ca6ce4 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 c37ece4ebd8..eece29e167c 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 0ee0e81143c..311904166d5 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 827bcb167c3..d0651c7fbe9 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 a5af541eeee..ba6f4b1f43f 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 76053f75081..a5dfaf4027f 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 9bf3ba09ce7..e7b0387698d 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 0d00275200c..4e5531ab4e3 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 7a0bf76e0aa..d4026e7689d 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 1a5b547e5ff..17c54a39763 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 9ed95597e41..b02149d2bcd 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 544ea860778..ff063d6b7e2 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 a63d4b292ad..4ec5a4fcb2a 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 b05aa956b42..b2c44c113ee 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 84a98ca0336..c488bf790d5 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 b8a40c3fc1d..19d1ce0aff7 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 9ee134d4170..ac507f59c7f 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 63f929754db..02332c9fb10 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 b2aacbd48ad..e6c1fee2c95 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 1ed18140bd2..a1e28966568 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 19f232ed667..35c27b7ca84 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 ac1ce2893e4..88a7ab5a338 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 98d628fe078..e7b59b07138 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 8f737e2e1ff..c7e919c95e5 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 e50989a15df..f1d91879f15 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 0e20a509d1f..17e9f973fd6 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 5191ab611ac..c1b0cd2c0ff 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 d3781f81d04..b86f4e26b11 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 96d792196ce..5ea7cb2fb41 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 39ceca41d08..c8bb89c71a9 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 d53bceaa145..0f81ecbf1be 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 e4d3622a562..9d59d5b9919 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 41fb04c8456..283ea3c45fa 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 f2110ffb2f0..feedf3be607 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 6ee78fec562..c4c892e88f4 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 83b4241284e..74dd7cbbf87 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 bce6758b0fe..8fd19f6b034 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 d593aec46a4..f7939e2b025 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 77f3c64752e..f7aa53b0a3a 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 a2c2ae04376..0b5e3c0df9f 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 4ea04de9e02..e439e8d7eda 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 6600e46a4ce..96df8c4e083 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 621bca8c430..775d208c1c4 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 7315e3c7c84..f1c030125ac 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 861abe0b462..a4d7cd50686 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 3f3f2c25d0c..d8878c8b351 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 4c7e82ecfef..d72cc992f6d 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 ff916930216..d2e016cb5d1 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 c8ac0d2e81e..bc9c71c5a61 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 190e5860ee6..64076434aa5 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 ec48e5252a8..30571c33865 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 ff61ad0e05d..b23a101ca46 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 cb3cd350c04..8b9621b4236 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 eaa168332a6..f4113f85a31 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 6e840458207..77062cbdd26 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 98ab8035023..a3c2389478e 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 b35d5cb1850..7bb828cacf9 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 f039e50914b..cbfd79671eb 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 a5eefc5af04..49748128258 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 7bfb87ede47..1e9569e8509 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 1032b5eaf2a..20a49e834b0 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 aa99e20a4df..195227ca242 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 a6a1d2b8aeb..0d4508c26dc 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 6bfb8abbe0b..66cae733d9c 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 1c7ddbd65ee..b25134bb79e 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 142261fe155..217b1755554 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 b8e622823d3..a3ac10a1c6d 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 4a90e9c977e..2f222b3c16e 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 eecbf0edd63..6bd64942619 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 a957db0675f..af4427c5db3 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 40572f82ea8..00c7b7a19b8 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 2d34d3b4e7b..484886aff21 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 b59d5622821..7c877f5c2a7 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 0a11aecd079..b3882c63c51 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 eb9ef258a3c..fb21a43356f 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 e26e2e1d6ea..0f2bde253de 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 bee53c89cdf..78917a5351b 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 f2bb0822d17..8dcef17fad6 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 d49e05a5416..9d563719a2e 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 e0989958626..00789c905c2 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 fa0d437a700..a4a0eb4d3e9 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 ea0abd87c28..55a3449f4d6 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 9f3c20fcca5..6b0492881fb 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 d9fae3fe54b..4164283f9f8 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 5266b81ab03..037df20b35d 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 b612c3a9fa6..b4c03047a7a 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 ebb0895bd7a..c0ab61d8568 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 70f6f6a3ce4..2a660281311 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 3c350e75730..260e54e65c4 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 2075f88ded2..40c8c7c3598 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 cdbbffb8d36..f2929fb655e 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 b50c7f61dd5..9d644de4445 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 d9898a71f8b..035354c969a 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 2916e81ce4f..4ba3a61d8b7 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 a3d5853541f..3dc591cac4c 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 7b43ec44ef3..667a5789182 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 0bc5586445d..6db8b041cfd 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 70093df9b55..14b0d7b3f15 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 3af23be4f0b..5d559cc60c5 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 54fd155b557..2affcb8ee2e 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 cb37dd3036f..9a3e478d108 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 c4e1bcc28e8..3aeffd025ee 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 6d332a28bcc..eaa0d10622a 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 efea5ce0f2e..dbe8af0b41b 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 980c227944a..e4ac9c0897b 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 df492d136fd..6e575ef17bc 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 25e02536897..e883aec371c 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 5f2075a0fd6..d3924ee61a8 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 8df144a0f81..7a8e3955686 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 58490569e65..975a3128f36 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 4d84c27cd93..8771ada45e1 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 d2ab3ad449c..edb77f1dc6d 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 25d0317078f..a415b0e3103 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 f3a7121c0b4..4a96e9828cb 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 9e0f387a7a6..20767a0d49d 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 a1e2c616832..e689cb859da 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 c26ba27a29a..4bdf43f8957 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 feda5da732c..df936eafa90 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 e233b5635fc..09ae235e6b8 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 287777e2ccf..9808c45aa74 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 2015b8ea734..31e00d0fce8 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 a71fb53a5d1..d71674fd40c 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 6362e679319..eaddaf49b84 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 5fcc2c5b507..c8821f3b176 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 869ad2766f9..d910e7bdc78 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 891be6b2180..f2d9a338560 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 28c9ea1e952..6e011a43ded 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 d83defbbec3..018ea4ac167 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 0547628b4bf..c3b1e27c77a 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 093ccbf6a5b..47759759a56 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 7392c931f48..caf422938b2 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 5961400e68e..da15d87ab02 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 4dc9e2cb7c3..035b61d0f2d 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 dc71e81ac08..72521e67af9 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 0dd0f1eae80..759e1b78e8e 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 c26b19c636e..86bb362f8dc 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 6f05125661e..ec47deabc23 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 2f6747ab4cc..a3aa3ad3fa8 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 fdc1d2943e6..7b6653ba832 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 220f2af2035..a2c2e4b11b6 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 e65aab2b69d..1ef84b74502 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 926f73fa4db..eb3626a315b 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 8e4e9747901..ef6bcbf92e2 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 9e959f35c9f..422a79cd0be 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 417616161e5..41e45a9e578 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 0e8ade339c2..a3cf7a51ed7 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 d87d1abca48..6172de132bb 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 b7edd1f6c5f..78542bb7b66 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 9f1595ebd77..888c6533013 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 3b2d1414034..76f15f3bdb8 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 8c684d495e9..ef138bb2ee2 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 e4129af39bd..397373499ae 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 783907c0220..f79b0f5b352 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 99dd4889213..c30a147342b 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 bbf22918a75..72e12e78ba4 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 951dd3ff85b..47bdd1ee0ae 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 2617b3e206b..78ecfd4efb0 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 bbe9fa8ad05..5bf63f2b099 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 5a85b6673f2..dcafe908d5c 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 1728aa50614..3f3d9d85c52 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 b09c8809382..603f755fac1 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 9eae371070a..a890ec39375 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 131d1a23b6a..a8c2755aefb 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 83805692e4d..f6aa8ccb7c5 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 fb38b41f314..de5f5ff04de 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 62eb96967f5..88e7585f802 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 a26500f69a6..4b2456f0df5 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 f8b42fabcbe..5eff655e806 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 19bb079c29c..28ed3f52036 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 10a5bbcf864..a2505427f45 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 7dc2e0d7518..064f7e1ccf0 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 4c32b88b2d5..3730f01622f 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 d11cf0b2f1e..b15799b54e3 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 08bcac67903..988e2bd1ed4 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 3393022a363..bcb2b53f081 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 e66050fceb5..2f46e6e790c 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 14a503f33f5..ded748be8dc 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 be2eb07a23c..46a2d4de20e 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 29a7a5513d0..0a76628a5b5 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 b515d65a14f..18ab9036c5e 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 cedce8152a2..e8e3ad8ac2e 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 60be8f275ee..c546030853f 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 c6b62387039..ac201fb00a1 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 b1767748395..22502956e06 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 3f0c97edfe0..cca4a54bda8 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 5262fa4837e..e66d5c1820d 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 20fe6fac2aa..41e3d0dd6b0 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 dd8da40efe4..72be440d54c 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 dffb4b52667..646fc1a3cbf 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 26d6c21f3a9..112b58ba65d 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 344ec82d976..bb6b18243ec 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 451a6f3e33d..cace2770de0 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 4da58cdfc40..d1501a562db 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 205072055bb..7f830cda25b 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 ff7da7190d9..e8b593b3145 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 6534d927f1b..41795c117a9 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 4ca1db564a4..2883c9caf34 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 9ed7a50a8e3..c0bd0823b8f 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 6c788971629..12d5297c010 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 81cd1338450..d9ec004e6a8 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 d26d7d9530f..e64d18c92e1 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 5316935c442..0188f405e14 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 4005e51e373..e041ba2f669 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 9ea1a6f0c55..a467518c04e 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 a28096c5eb7..1f5b1eef935 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 41048683c92..2dbffd829f8 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 71e1a81135a..886535555d5 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 bf6f8fabafa..4df8ad8442a 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 134d85fa171..0e42cd7e535 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 f9fa1cab502..64760338f35 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 d03679ed41e..2add8663555 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 a6c49b3c396..79428f951f1 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 426d0faf860..2652e33b76c 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 b485458be40..ba043bf2a71 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 692e5526085..ee14ce7d26d 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 f5d358854f6..f5624e33edb 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 1e84b39a366..c08b4e4c88a 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 8545db90e27..5bffcf8e92c 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 827d961a093..16ae94c212e 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 7f6c4fbd935..1d51dca42cd 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 b272a299cf5..12d73623e75 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 10e4bde103c..a52aca1ac0a 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 c7ef2b89611..54fa59135b3 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 92869856545..9a05f5a9f87 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 05efb845c12..85e70c78ede 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 ea6befe498b..ed899a0438f 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 2b2f84836ea..bc373633503 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 2324a861b38..612e7c19f8b 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 2ecd66910be..93f01ac2e3a 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 85c6231daa8..8c95ca4ad41 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 0e5c4dd1ff3..8994223fe31 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 e27f594b0af..8d271d5a95f 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 e3d6d19b803..80174b1a83a 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 de4680927a4..1205b53ccaf 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 aa07911a697..47db73ce0de 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 50a93fce20a..56b5a1b8181 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 5acd5bbdcdb..ddb51c9e682 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 e874ca28891..7d419243472 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 beee3137ef5..b7440f655db 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 2df50699a9d..c1b6659e1c0 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 e795c8aaf18..0d36cd6616d 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 a1986b2bf2e..b5448839794 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 8fc90b0bcb1..93450b00163 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 5fa99363729..840a53591a8 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 5626e244484..e47b7c07d8b 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 1ff997372ed..20933c9b2fc 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 77e5a524aac..5d5ee195381 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 b870ff66309..213679b320a 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 536848d3aef..8701424ea60 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 0e17f33f72e..fe09461bc82 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 4e37544a99a..b912a80f75d 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 b49932887d5..7f010ec6d39 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 c624acd73da..7e01f818e35 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 e75e2caa37a..aa1e2eb7422 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 a59a6352af2..3318ad3438f 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 71553fabc8e..8b0c5acc723 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 baa6551cc7c..c1360ecbe30 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 0caafd7e27a..e747bf7739f 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 93b19470ac4..a1cae2df1d7 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 8a6e8ec611a..b7d5f132045 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 82f32c34407..83091368aff 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 9c3b8ad33d2..da4d15c28aa 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 3ee3b189939..af709c755c8 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 99ca3cb1ccf..7e085d06307 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 e3675db04d7..3755746aee1 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 04420d5c4f2..735baf58e5a 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 5c5a095c8f4..525947a8c74 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 8f3d88b58ee..45eedf8a154 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 8cb58a7b74c..0e168601c4c 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 ee7645653e6..fe7a92bc270 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 70aaa112414..a3181292147 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 0380acba1ac..b207bd07a42 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 1a2fa055688..5fe5f743fd3 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 12581599532..438b61136eb 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 9ffc0215fd1..069fc238f72 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 08c9932c36f..e7ee2d2dcd5 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 bad39a1cb97..6586496abbe 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 827d406a1b5..2a400754b46 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 6be24fb5a2c..744497c22c4 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 4542eb45c82..3d16f0a57e3 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 c9e69c44ec2..3d055f8f975 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 9f18d2849f8..a88faaa6f30 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 e7f078a1a05..77043f37134 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 920e56fba7c..21231c27873 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 bad90d9e827..b112a9ea4ea 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 67b5b0e2eeb..64f72986d07 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 17b11d6aef9..1dc16fdba40 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 15bf40e63c8..e6cc998352d 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 68045ff0584..09087663b47 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 c34e1458e4b..77f6a22e69f 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 d23b07b2756..1634676a60b 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 33c93bc8ac1..c958d23e725 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 066f3270ccd..fb6daf00f66 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 ffb01bd5602..2febfc481e0 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 00075d4485f..2d8c6c71071 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 7fd27dd7edf..5bb116dece8 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 d86ec38ccb7..f6c23712188 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 f0421295836..56128f0e886 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 3c92e840f43..65fa6b1bec6 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 e8ebeb102e6..40421674a4b 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 f37c769d20e..a56f0caddab 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 ffb86d4a5e2..fae9e616e5e 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 95f944b7087..4dcb53e98ce 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 d9281f08eda..4a17f933515 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 276346ae79b..2ec58b86125 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 ab03f1cf7c6..632ab82918e 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 dd58cdd4168..a98e828a52e 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 c6097a01cc4..9c7f165c6df 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 0cfb02e81d6..69342df235f 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 d24b23f64bb..c28d27c1b8b 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 c10be48f3fa..cee6447abc1 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 65fd0f7de50..51bca8fbbbe 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 73f4eaed7da..9dee62697c3 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 0b158b96742..8c6c9f30b10 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 112fca24194..25ece71c11b 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 b89947343c9..2ba00671b48 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 bc4fe97bc7f..529d7990a86 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 fe2387744ab..7f386464dc9 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 432e729ef20..e7e06479eec 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 68e8337a33d..a15267b7d85 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 06c9338742c..5d0cc555705 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 e66f94bdcc2..6d7c66f6097 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 186e071d25b..d31b9a811cf 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 8bc343e1f08..9ee9692c655 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 d2cba929259..cf50b8029c2 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 0b1579a139d..4845aa16a37 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 7fe8bba6873..089e1e60a11 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 bfe7f449ca0..5150ddd0404 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 67b1033c51e..012ce4f014e 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 dfe4952e1a1..7613f9e2a34 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 d98adef87a7..19e84365fac 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 b06874149ed..3aa046f234d 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 6901847bd8d..3c2b67e3425 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 cd6111ba9da..658173d3db2 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 cbde894173b..721a57e7822 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 8e068c909c5..d4f2ae0517a 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 c399232f315..1069c0bcdf0 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 389eca09c42..8c2da090269 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 11c20236324..f72984f9fc0 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 fcc9265ce9b..0f2faef86df 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 9746cb168aa..d07f9746ee8 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 2427ebbfdb0..99a8a1e2701 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 3aa356823c1..a035657a090 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 cab1228aa56..309284e5f62 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 a8b4893c995..73aa818a2ad 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 335bea82fc9..708a3fb2616 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 54d5ebe5f14..c67fd4afc09 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 797a5432f97..98a14d877e8 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 8bd46c3dc32..2f8398531c7 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 58a2871e027..19247e1a7d5 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 278638caff8..161ea29b3b3 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 51e77959d7a..f649e5467a4 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 b42180c7268..3000f9fd17b 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 1cdbb4ff48b..01c3fc270fb 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 300d0ead4a5..18592124c24 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 b61c7100828..7e6879de9a1 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 437bd3bc4d2..4cbcd8321b5 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 83d70830b11..2e5c1208210 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 5fb850739d8..c41d4ba46d3 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 47a4a4b5f85..c113e6034cd 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 f02d416c7e6..a34c49645ce 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 eb8da25bace..87e16f62987 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 9c062482a4c..da2fc20510f 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 4907cb462b6..6d2944282ba 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 30bde9a297d..79e3677d65e 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 f12fcf4220c..2683525e7b4 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 002fdb63273..c3f8079cccd 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 b911aaa57e1..bb421c725ca 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 4d07f2a3ce4..612fc32c014 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 354eaf8b313..c5503976d3f 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 25b36c798c5..37db2b31355 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 c184f35734c..cbbaa562abd 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 8f5040152a2..b241cd6db15 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 fee815a7e6b..f1330c2abe5 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 2cccf32f298..396afe30dcf 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 9ecfa88053f..cdb09d66067 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 af8e802c5ec..b11eca2b088 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 c6d3b3458e5..55baecc486c 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 72562399330..f1f82999188 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 c9d35e9d2c9..4979fe29e0e 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 5fe585dcd83..5b9e70ebcf0 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 f15defd63dc..df9a9c75123 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 14af98cfb64..a894285f729 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 999f5740715..0b197d104ed 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 ced930fc64f..238191a9343 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 bbdb49ad401..bda260bdff2 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 5d6cd4b038c..a75a8ba9eb1 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 9dbedad1ffc..47fc2f3a6a8 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 fe93bf02445..b8aa5c74302 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 3f32a61c081..1f06daf0623 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 3a8959f1be6..924a9b86d47 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 477bcb105f7..f2639b31d15 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 058ad0c5e81..5064357a7df 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 7ac91964a98..34a296b0f9d 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 71ec8fcbc9b..e033e82d922 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 1f905b103fe..d5443787d39 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 88322708b27..0bee2baeddf 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 d2ed380d68a..ccff3d3357f 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 7fc724bf90a..fd0a0a99f58 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 e5fffee131e..5914f65ef69 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 c92f6b2a0ba..7bd92876456 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 ce2dca9e085..899ee3a8ae8 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 4667e9b8314..9404153f4e5 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 716f383040f..687dd05b61d 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 9424e5f3a1a..e69c227f148 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 c0367c47902..e42b37195a4 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 c8825f4ac3f..a080fac112a 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 ae56b54ce18..dbbfebeaccb 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 e1becfd1936..b80e4de5041 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 ec9807d4e00..989070900ca 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 56a3c39b7b6..51ce17c500a 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 d565a5d336d..b6334272f23 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 74bdb3d8b88..528e9ef35f1 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 1ffbe69888f..f9bd8cc31b4 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 8763234c5ed..0112ca28469 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 cb148c92198..deec8c23ab7 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 776b8444b82..9b49f442d15 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 813bcc27aca..1813d782d31 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 945464dbdec..61da7b4ff30 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 b2a645c88f3..b326481c55b 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 ad6c71bd19f..f689cd46858 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 45ce2f6a7a0..0bf49d4b906 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 47b36c12291..6a5ac165291 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 dfe9a8e8e6f..ca354ff2331 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 02718396e5e..282d86ce419 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 fa704a6550a..3ef01a44315 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 b4ea3c2d388..41fe28defaa 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 dfa22fc6e23..6ed947e5c82 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 1dc8f5255ce..689703302a7 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 cbf2833a4f7..d303ac221dd 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 cf26955b207..96b337def55 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 b972152aea4..4dcd0715704 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 f8e6e1bf14d..d5cfc488151 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 46337918f84..6be45690629 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 893a1f3469e..a3cb97cdc2d 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 843fd3d13b0..8a005be52e2 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 38fa5fa0894..10dc4bffccf 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 ea16d626af4..ac605f42b0c 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 f9fd7f242b6..3d2836e1809 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 ea3a33d55ff..8f0caa7c548 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 361802f312e..6f5a4b7b64b 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 18b587bac92..515f3dcbf65 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 621da91f4f8..8b5bf65afa1 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 b2e3deb4008..003ee804b55 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 421eadca51c..b3fdc2825ae 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 2e1a8d826ce..b6ec6b4ca0d 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 0bb36c1317a..1314b91f982 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 58fddb7a3f4..e15d86e811f 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 a3ac3af985d..d3942ab4a32 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 23fad3683cb..93361d41e2b 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 e800bd7266a..c90342018f8 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 7452790cd60..f2635936cb5 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 291c774c383..91ed6f55ee1 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 3a154b857fe..14ac59d4621 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 02eab03c8bb..83b50684fda 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 d4e799c2cf1..35422cf09a1 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 bc0235ec5b3..ae32083da39 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 5a01d9f9e2f..3160c4cee4b 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 a08c0a59c07..6d636f36b3f 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 1cc25d93f59..497347671a9 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 eba60bc6e34..e3ca9ed72e8 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 03aa5c0a1f7..4ab05f170ca 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 572d4b04b87..5b59d3bfe29 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 91b7e7c5c0f..b35df8ee7c8 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 4cd7a467737..567f6f0e513 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 2e81da3409a..a6972e8881d 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 5827f307ecf..50d9ba3163d 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 366a5eef0ad..f514a95c042 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 38a320543a9..41d80ebccf9 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 ae124d6c03d..1f651f746e6 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 ce00bcbc888..1c3d56fe7fe 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 1314fda5099..33dbb40f78a 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 d2f9e90c41a..79637bfb124 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 49e476a6876..3dab05942b9 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 20f4cc7f544..387576f8845 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 735a1869c34..ecc6198cb88 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 0f8b586a9c2..385df6a5063 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 f285f81f27f..a00315e1064 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 b4173ebf0e9..83b6314c4ab 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 307f4c929cf..840984ad073 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 8ee3de2d77f..654f99f7ea4 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 e55131306dc..31fcb1d7c2e 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 1fc849165ef..eaf1e66cff4 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 cac1a5f18ab..72635ca641a 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 d6b18d6cba8..f54ef55ce17 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 99dcdbd0c88..c91d85fecd7 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 e52fda34083..27e33c81607 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 0f287251582..73daca18c6a 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 b9ea4eb276e..204a3605bb8 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 2f3b3b6e84a..453ae542b2c 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 94f100abe86..b15024b545c 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 a2a45826d9d..79e6f8e5571 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 a108f5fa983..ea743836c74 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 d35b1d8c902..3522ba0405c 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 a790a6c453c..c507e07c717 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 fcce2e52a21..d7dc76b6ac9 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 19836ffa67f..00dda3ebc02 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 9c2b7bcae39..4de7af3f862 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 01f70af4a15..0bc14a56e71 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 565a459818f..a45a59e410a 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 8d42cde1c05..9a884fa010c 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 ca3ab0bc882..1e99d4b288d 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 04ffb48f396..328285ab67b 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 1c8e8476987..79fff1b2b4c 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 d49b5280181..cc635082c35 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 2ff29a27f31..53f3f6c6463 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 9cc50405bad..b02a9db3fdc 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 14afdec3b71..220a086e0be 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 8a6dd7fb369..392c45ea886 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 f341fd587ca..afc83879cb0 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 7f431ba92b1..777138d44ab 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 c50ba514f2a..3391533b081 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 58f5e15cc1a..afba0e38301 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 0e60c957d9d..76656e9936f 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 c8406c9d084..20a35f1afe7 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 279ac3b103c..e7d35829ffb 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 ab32a64e670..4071178c7c3 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 d7282317d95..7c93000790c 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 fab670627ba..93a334fc986 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 8d6082d74bf..98a852dea08 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 482bb94ac2a..689a6678cab 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 4e43270a5e0..c7c8aaaf16a 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 063c84d4ff7..ac07a2f77ad 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 107b4351505..f3aa5405278 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 d0f358c5902..11c5a676bf6 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 7edaf9ba978..c81b6f0a0b1 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 3d26e8315ae..5e40c89369a 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 bd620d4a18f..1824ea2db41 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 76a506faee8..1cf2c610014 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 ff67748d64c..ff582b33cef 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 7a6b4e2efab..dbf1a941e00 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 5f64e661a9a..dc87b6c2fc7 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 615ea9ecd7e..5f6584ce250 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 3fd00e88a0c..721f7037fce 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 9ce41b59861..fbc788d252d 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 e6bcd6dd00a..39cd0e0f1e6 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 5b8b4f521ee..7c0b36b50fe 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 e0f85757afd..f299e02e2d3 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 e3efd8c8331..e1cdacde6d8 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 7d3e2f22d65..ffc82ee13e8 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 6ad348176ba..638d63cb487 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 d847c6df24f..d9fa4ad5696 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 2e24100edd0..1e24895af44 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 9bd734fe094..64f1d636a1a 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 2bd4571ef93..c2fa31d7b50 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 491cce7407f..9d2e675c757 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 eb553fbe73c..f0d5d54c8cf 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 8719138f3ac..1432d2d21a0 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 ce600473cc5..ca2059a4d19 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 9c83056f6ac..cf16d587e87 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 d1acf936a24..4eebba28ef6 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 f96afa18115..23fac51a347 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 b235385396b..3fe8f129b5d 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 2174dc275b5..00c843d2dff 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 80bc795b536..f64182f6e4d 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 e721bb669ed..2713004343b 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 39ffe768657..0bf29beb0dc 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 f886e84254b..664ae861442 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 189ac690758..3711d7cbeb4 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 be44a9d8cc8..cf349a20571 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 d182806c4ac..ecbeb002f04 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 515bd68d011..805dc6638bb 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 ac72d10f07f..3e066b48598 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 efa38286e7e..2cf463d1cf0 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 3a58d80f64a..62ce608a911 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 9275ef34968..eb26d6e36b7 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 9aec23a687c..d4446b271f9 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 10197178381..5997916e2c3 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 375baf12565..cc2d1b6c2e8 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 f2ffcfbf8a3..707b7f27860 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 0d1c116786a..d1ae97b550a 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 59f4d1ca21b..7a470037ba4 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 5c77f9ecc70..497bdac656e 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 8dfbb8ed968..69edc40b15b 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 4f9b0f4d126..51ecda91404 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 47153dcf17f..c4e3a2c97bb 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 b071b354d74..1d9401f6cfe 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 9f1f4a7200a..783e23a8171 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 655ba019e8c..e8e36d04467 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 b2f1581e76f..120ec241d60 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 7c895233f77..38ea8c73147 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 815e7ff9a25..3f35b0dc9a5 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 53cc96be388..d52380c1025 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 e145b26b0c9..99857fc2f7e 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 e5f55b20dda..9f7c703fe4b 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 cce2307bc4b..20b2ac347f6 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 c65204d78e8..682405a375b 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 a40b0e8c7d6..0dfc9f1ceb9 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 1e0f1c71df5..7894eb2982b 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 6f0b4ae5fd2..7a079d9ccf2 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 db068e35056..0b210bc780b 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 e7c4bac4abd..a97c9d637ef 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 aa691d7766c..3f75c391115 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 5f5461f2d63..0a0afe3d71b 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 920ee1120a7..89b0ac591ea 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 dc689f8d617..ef6dbd06470 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 4b692c669d1..8ce03e2e8e2 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 e291777483e..7ed6493b843 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 57aa663a348..e2982bd0145 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 8896f34f976..20834bf0bf4 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 d40baaca132..4cde3f764e5 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 09ae4f812d7..85bcc19032e 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 7008c033f95..568ce57ed62 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 384e61aed2a..f93d8a5e295 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 7c30ad6c5d3..7cdf8bea4ff 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 4dd2f92628d..dcf908cd603 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 bc630b2947f..9826b11ec45 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 1902df1060b..aa863bcff0d 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 6040c8655b9..1566366362a 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 cddfdc5dc9c..acf9c38e632 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 c3da00ce951..7472898b7ca 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 726d9f13eda..d38b69f2248 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 ba8712f0575..4b407e95235 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 2da33f942b8..8f7b72491b8 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 dfc116c97db..467a98a6660 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 3e7c626ddd0..b112bb7771c 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 c778ed1049f..1c20b884223 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 d14c9db419a..8309f03bca4 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 505447f454c..90a55ad48e8 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 189b9365105..a7bf14454da 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 2fbe16a6e7a..bae5247ffbc 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 5baf928352d..79f4ce6c87f 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 ee8ed2f6de3..7cf061018b9 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 71750ccf088..9e800dc2e4a 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 d3587100501..a607cda511e 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 36da259f82e..9eeddd357f6 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 d7e0d0d732e..4c01cce2d3c 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 26940a57c78..740eaf3ea1c 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 3d2c3a5e911..21255497503 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 4ee13acf647..290c552309b 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 7b786c7bf7c..05e979ffb0a 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 5a277fc7ce8..bacb9fc3305 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 7769026e092..ea36c4921c5 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 6c633f848c0..91267b43480 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 7f622a1e25f..66a546abdb4 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 061d2b065c4..3d27d5bd393 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 1a75472b801..390ff742724 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 c7b5535d03c..b68649525c2 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 bb7fbf55cbc..461f3e24330 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 7f06ddddcb5..d49004cc0e3 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 c3048601595..482951e156f 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 c0f100fa62f..5959fba5fa2 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 ce16e1b523c..2b95a9ec0c7 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 b846c95f5fa..e24e4537837 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 1461a54d147..39f016e9d0e 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 4d1a55eaa09..79c89406fac 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 f0f6389a061..ab8a20822a1 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 6382a830dcf..32ee6964321 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 1e4076b8439..3968b0e294f 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 9e587137922..ff9e64ae78e 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 e9281fec3f7..7a8cdcf6c6c 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 9d371fbabf7..c29a97e857e 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 f88945fa281..9268a50a14d 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 45f3f91cd6d..d9f6c877cbc 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 105a03bf757..33458d049a2 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 a856e48438f..f27b1cc7f1a 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 feedf3be607..96497388a48 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 82db3d4224e..2f5d9bceb8b 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 1086160e77c..cd4f536e14f 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 2cf2c765e8b..a0f64eeec74 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 23fdb38aa05..0a95de783fa 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 ab4545256ae..197e424ce86 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 8fd4b4fe94a..b752b82d087 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 598e3765612..cd791434f90 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 007ed6517ef..9b4d5a961ba 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 41d78e6e7c5..1b4670cf1e6 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 ed26db3d49e..d183dcb0fa2 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 12484a655d6..f7ed3a73fce 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 93fe285dcfd..f284b2eda1e 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 0c145963653..a32c375ac65 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 77b0bf9b313..60042b92867 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 5f08d0a4750..9d0610c139e 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 0d4d373b2b6..5246fb49eed 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 c6ca6db32fb..67dc12565d8 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 5f79652621b..7fa1638253c 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 b47cdb98161..ab63e6fb319 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 9aa4544f5c3..7d8a68a9707 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 28dade82d98..1af7fc0490e 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 3b962f38caf..885164d7a34 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 08f20778d70..ae9531456fd 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 93a75d44e9b..67553ef608f 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 2790d9a93ac..59cb270c616 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 705ac59b98d..25f0545c340 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 756eb65d4f7..3a7223ade29 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 198a5e9cabc..7206bea110b 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 2ae9cadf1ac..3d5b4259aa9 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 0f7b3c9ef80..a8a55dea48a 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 a3ca8fc7629..54c9f815008 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 8c7edb26d46..2c7c71d7a69 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 cea3146a3a5..dde5165c5c1 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 cc3f1f23727..4bd75910163 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 1408e05e738..dad8e0bf496 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 145b725fc79..1873cd7dc55 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 3d4a03fb0ac..37b0bec207b 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 378be778e6f..16592c9a8df 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 f67ee2fb75a..7afde3e5f78 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 7a5f0161fb4..dda6c1f4c13 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 2f6e6c1b3e8..14d982ab1e8 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 d8f769a3e24..272fa28905c 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 99fed7d5d68..7cd2134988d 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 e15acdaf5e3..31dd5cd63d1 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 94d885592eb..369ecb69aa1 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 b9785068f21..aa463392e55 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 25c11a9fd4f..e4840dae86d 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 8e2fa7e3d5a..c7de2943691 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 82d20ff78c2..e9e2c3b7ea6 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 80642a373da..622adead803 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 d2f842956da..a64e8cf7fd9 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 b07658e72d0..88a472716cc 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 d6483902602..a4e84e83105 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 db7e89dce82..43ddc888d2f 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 0e840e1d003..97c996d9e59 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 cb4ecac61fb..0bc54519ce9 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 f5cd03882c5..2bfebf145df 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 1341bd75d1b..6f63fab3fdb 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 7b4d1f96a37..4008e6a17b1 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 db635828529..5b43289e403 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 4522ac4254a..eac14735074 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 b29b088a815..ffea98b9050 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 ad04b8a0eb3..48b19d7bada 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 4f757102d53..f0183a7ecb3 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 b28044bc3c7..e73719205f7 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 760e1e4fa4c..3a2b37487af 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 e660e57fc67..7ea5e0680b9 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 ead71db8c27..c00bfca3564 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 77efc78562a..5339eff055e 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 00aa463349c..db43c022822 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 6590028d766..4677ea8d5a7 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 eb3e5a95c9c..dc65008c3fb 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 2211b300104..c4d2efcb63b 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 5339eff055e..badbe8b8651 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 23e320fe153..a7e04f874b4 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 ef1b605f4d6..7c68be83ba3 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 ea37b8e9470..9d8ef6bceaf 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 f2a82afdc2d..5bd5d83e1c0 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 ca91cc70512..a65c1acc512 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 9be294d5460..22a016e49b9 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 27e78507bfb..460676a75a0 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 331dc3d3296..c1ea3c2b761 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 d0aeb4f4968..2d1bb89d23a 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 e383f1013ab..d9b3df8c01b 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 684285a1cf1..c500ddca85d 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 a0f64eeec74..4a2a2cf45fc 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 d842ae39ae1..23a8a808f43 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 1982e80dfe9..bd611e04c37 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 5be354c78fc..9bab1ff1f36 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 acfa853d596..8a92f69e574 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 9b540c7aa15..a6437ef9ee0 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 43af9fe3df3..31fb6d30f60 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 f1d91879f15..60a4f0faa9c 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 c500ddca85d..29484b671ed 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 4a2a2cf45fc..2aa04f8ae15 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 bb29540d6d9..61d3479d8f6 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 874ac6334ac..ed94b6913bc 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 ab8a20822a1..59d9508ac33 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 2aa04f8ae15..cda5afabe7c 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 aeaa0a62c4b..c8bc76fbe83 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 357e0e3026d..94c931e3514 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 df9a9c75123..c3b93346916 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 cda5afabe7c..4f0963cdbef 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 76f15f3bdb8..f99ec2f22c0 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 4f0963cdbef..c84b57a09f3 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 f0d72af23d5..55b5878f667 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 3cafa899024..e5e5b8ab7ae 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 3f48e2afde6..581ce6b461d 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 70b79becb8b..1749ea91c5b 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 a7e04f874b4..fa6deac40ba 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 a69ca7ab8f2..4abf34e6661 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 032c765bf59..5825a3ba91a 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 afb8c3934a7..df82ba6076f 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 d9b3df8c01b..05b28102726 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 8a92f69e574..fa78ae94416 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 c8bc76fbe83..b8bd3841a78 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 94c931e3514..d53f145e6ff 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 32b4a7e2b24..da1e173b4a8 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 00000000000..b0ad4660d0f --- /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 00000000000..f1c20e5346b --- /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 00000000000..392dbf35f5a --- /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 1a5d992ef7b..448b654c86e 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 66c02884a7e..4d442a4543b 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 9ddf336c060..67a65f752b4 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 00000000000..170c2851d08 --- /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 3f9bfab3304..512d0285ac5 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 00000000000..b63f0d44452 --- /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 00000000000..130a856826e --- /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 3604eb66ae2..bf3c059f81a 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 b68c7784d05..cf448bb35e7 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 78239148a4e..60a3eb65ca9 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 ad2f19b0668..1f4968f24e1 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 670bc23ca1f..1da4b4b4b49 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 00000000000..2fc98a70ead --- /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 f7a6bfd9c7f..a0a9d087d1e 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 25f0545c340..705ac59b98d 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 3a7223ade29..756eb65d4f7 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 460f01e301f..a82da0ce24d 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 00000000000..ff0383c7f01 --- /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 00000000000..f9607852036 --- /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 503530e728a..1281784dceb 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 dde5165c5c1..cea3146a3a5 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 a0ce81495a8..598bc77aee9 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 00000000000..bfe62cc4e78 --- /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 00000000000..02ac8ecf27a --- /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 00000000000..1e1e8897c30 --- /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 00000000000..58c62fb64b6 --- /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 00000000000..485150d2928 --- /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 0047c762a9d..edf38b4fd3b 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 cd618072592..2f6f42311c3 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 9dd4657291d..8d88fe9cae6 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 c4d2efcb63b..89fd9add69a 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 7c68be83ba3..c9588c1efa7 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 00000000000..1074236eedf --- /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 00000000000..7df239facde --- /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 bd611e04c37..0684e6daafc 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 4677ea8d5a7..91714e647bd 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 00000000000..1dc41f5bffa --- /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 36c0f52f41a..5e1bbc76fb5 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 00000000000..c25be28fdb0 --- /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 2bfebf145df..4e7b0a0cd2a 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 c6629982d53..9db7e270e61 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 8b295634c99..71550fc37b1 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 07882d8dac2..38ae09ced93 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 df1b8ef165d..e28d75f017d 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 8986f8f3995..38e4aec56e0 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 fce530ddce5..f243eab8fd0 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 b15fa32cb13..c8fa6884563 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 d6e78e87e25..a8ff775a227 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 61e0c70b6b3..a1252d67fff 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 daaf790a0c1..207194d79ed 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 d68650fb638..aea226348b8 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 d2e016cb5d1..f97977a7400 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 00000000000..33bb16d7830 --- /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 6ab6d937b51..f9b311a3880 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 29484b671ed..a64e0dc38e7 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 c84b57a09f3..fd44b46c64b 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 61d3479d8f6..9bc9870be10 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 c8e84016a28..a7c8898659a 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 00000000000..43914f489d6 --- /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 d5a3395440b..8e03fb82b2c 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 244c22d2486..e160ea8826a 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 12de2eaba1c..8049ac4b0db 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 1c7e0201135..9a8b9643cfe 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 00000000000..bd59502a3a1 --- /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 6a82204a261..6ecd4af446b 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 7d8a68a9707..b3f99038b59 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 e5c8c6af14b..7df2db1b17e 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 b65665eb2c9..37bdcd82afc 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 1074236eedf..c8655d91c5c 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 1dc41f5bffa..7118c94c5c9 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 c25be28fdb0..651ee17bd65 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 c8655d91c5c..50fb1dd5c14 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 7118c94c5c9..45452dc84a0 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 2cf463d1cf0..cd5d327f2c2 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 fd44b46c64b..45296e42502 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 581ce6b461d..ebbd1771e84 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 68d8a88df47..7afdbae5a28 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 8cf9af0e3b4..24ea3d281bc 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 4fd3b062a10..e84ed35c763 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 45296e42502..5328ca322c7 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 9bc9870be10..f3cacfa8888 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 b3f99038b59..68537aff298 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 a189199bfb2..7448eb27ae0 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 b564cff7338..a5b879e5661 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 0c116568780..29af1049fae 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 4e7b0a0cd2a..d2cda1f1d07 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 75aec037a25..1179fd90868 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 dbf1a941e00..e7876c953cc 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 e69de29bb2d..c2d23cc4bec 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 5328ca322c7..be88d6c60cb 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 356c7fe5741..a49e4c9c057 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 d4393d38c97..df9e9f9f6c3 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 dd5401950e9..38727ccff06 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 6538d8e887e..aff79acc2ed 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 753d565a82b..e9f48f6a4f8 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 17a6709496a..08a0430ee2a 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 68537aff298..5bd5b7ea765 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 207194d79ed..933f0c07999 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 33bb16d7830..869bc61d7a3 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 bd59502a3a1..734d9ec7fc8 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 df9e9f9f6c3..6ab11430766 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 60a4f0faa9c..58e5558781a 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 a64e0dc38e7..04a68bf9633 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 be88d6c60cb..7aa5090688c 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 f3cacfa8888..69c6ded5806 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 89fd9add69a..f138bcfd5a8 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 109a2b1fd45..e01af8d183e 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 00000000000..bf50b4f23e5 --- /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 00000000000..652cc46a7b3 --- /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 00000000000..cb290dc46c0 --- /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 00000000000..83284aaeb7b --- /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 00000000000..0c29ad0bc66 --- /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 00000000000..cf756a9f492 --- /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 00000000000..e50f618575b --- /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 00000000000..08aac57d162 --- /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 00000000000..7e2f4f1ff6b --- /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 00000000000..70924bb7bf4 --- /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 00000000000..5d601b37591 --- /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 00000000000..36080c9f372 --- /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 00000000000..08f57d88bcb --- /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 00000000000..bb38d2b9b8c --- /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 00000000000..7c129cba3af --- /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 d1264010b75..2d7964f18eb 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 00000000000..b0ad4660d0f --- /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 33644469359..b6296d1acab 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 67d720aa85f..951263d53f9 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 6379df936b8..93b61893980 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 00000000000..80792f16635 --- /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 6a795dee13e..d962c793121 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 00000000000..67d4f852625 --- /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 ce1f1ab3caa..98ab98e6b17 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 97e25e28965..830ae0fd13f 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 cb5db0b8348..04a08d185b3 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 cc6d22945dc..3729f7f556a 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 7a2b8832864..1f0b344a32d 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 840bc8929a7..f5f41a28a32 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 c7079fd6219..7d05a366cf2 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 9aebb2a556f..0717bcfc39f 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 bd47a637761..2ad613cde68 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 00000000000..5a96b853eb0 --- /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 00000000000..1959f769d3a --- /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 00000000000..85da5b3a4ec --- /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 00000000000..301239cf31a --- /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 00000000000..fc349a0d9de --- /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 c7b85c19592..32bf473ac38 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 830391f58f6..5e40c8ec5e5 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 1405112c070..62d24662ab6 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 00000000000..d8afb1e7aae --- /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 7d6583bc1d5..d6763d17e2d 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 52b8798c0f4..6942f683fa6 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 e8f4df930db..e98df7466ff 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 82ecd4a3250..5155a42c4c3 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 be5d2b7159d..79a46e1861b 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 00000000000..ea78f0d0d5d --- /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 00000000000..0e024a0e1eb --- /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 a260efa5bd9..a302572ed12 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 00000000000..89922b66f03 --- /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 06a5b7e69dd..0c3afc88c94 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 00000000000..eaf7601be9c --- /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 00000000000..98345b022f2 --- /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 1e962142373..d033da4bae7 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 00000000000..c78905b09c8 --- /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 e07adeb0a36..be8966d9556 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 00000000000..6e084d1a9d2 --- /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 498b5b2f12f..b569cce9239 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 d37aa3567d1..7ae83b550e3 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 00000000000..2bab2e43001 --- /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 b70d3f12a83..2fd772c8619 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 00000000000..0e024a0e1eb --- /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 d44142213bf..f647dcf7b45 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 d92d28d9484..559382cf209 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 00000000000..d30f9e5ad4b --- /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 00000000000..7f0d122f38b --- /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 00000000000..2a75d11a9ec --- /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 00000000000..73628c8646e --- /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 00000000000..99e7574e4b2 --- /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 00000000000..941ed18660e --- /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 00000000000..d5615815dc7 --- /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 00000000000..3043ec6ec37 --- /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 00000000000..e04e73aef24 --- /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 00000000000..084c4b50e45 --- /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 00000000000..caea115b7d5 --- /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 00000000000..1a206258e24 --- /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 00000000000..7acbb718d17 --- /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 00000000000..61f49cd5da0 --- /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 c7e89c368c1..7345cf46295 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 00000000000..0224d663a83 --- /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 00000000000..b8abb48af4e --- /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 00000000000..274dd46488b --- /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 00000000000..65d7d9e92bb --- /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 00000000000..8ad9d89b07a --- /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 00000000000..82b9a7aee88 --- /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 00000000000..9c62686e190 --- /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 00000000000..ec1f719a6cc --- /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 00000000000..4fec1baba7b --- /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 00000000000..3d018a2292d --- /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 00000000000..6104aa7de72 --- /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 00000000000..7e4a0eed013 --- /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 00000000000..718322ec171 --- /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 00000000000..5de551d5d0c --- /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 00000000000..648f156e864 --- /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 9683c5d7c36..58d57b28056 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 00000000000..cdfd651beec --- /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 00000000000..750e14b1dae --- /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 11e11ebc6fe..a3ba5185371 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 00000000000..95083102273 --- /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 efdd75b1481..bf927b7f1be 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 6d1ad1f62da..261ca951490 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 812de425ef4..c9e61dcf2e9 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 3c28f1d25f9..8f61f968dba 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 1e6488784d4..7b0f7232976 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 00000000000..a16deb2fca2 --- /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 a0a9d087d1e..18c4e865a84 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 ce9d2e1e88d..9b75a0061e8 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 2b63840d001..fe773f72be9 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 49ed34baf76..9be270a017c 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 5f6d0c41c13..2d4ce1ea6aa 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 00000000000..865155c0494 --- /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 b3302389c88..86920906003 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 9ab6156549d..cf842efe9f6 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 6eec3ef0eba..6248890389d 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 00000000000..59db5a62f86 --- /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 00000000000..df9b9935dc1 --- /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 00000000000..1f248099df3 --- /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 00000000000..7bbbe660feb --- /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 00000000000..a9039d2e410 --- /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 00000000000..676a5aa413f --- /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 00000000000..07b20245c16 --- /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 00000000000..01a4e89c9f4 --- /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 00000000000..5f5eeaacd11 --- /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 00000000000..da1dcc1d6fd --- /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 00000000000..e3bc994b6ea --- /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 00000000000..af97681ee76 --- /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 00000000000..0568cad3f6d --- /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 f685297890e..e82172f92f8 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 88ea988ff1b..03b17b3ba8b 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 fe36e4296fe..d8622cdd2c1 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 00000000000..d93eec8aed7 --- /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 00000000000..8126b6ea5ae --- /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 00000000000..e990fb98dc2 --- /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 00000000000..d8aba082537 --- /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 00000000000..6eeb4f75a3c --- /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 00000000000..0ed881853b8 --- /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 00000000000..3d28389ff91 --- /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 00000000000..ff0383c7f01 --- /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 00000000000..934f56a420b --- /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 afaaf4edbf5..9d3c97ad902 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 b8dbeff27ca..eed288a5e39 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 00000000000..1d9fb41fc8c --- /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 00000000000..9fc23b3e394 --- /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 00000000000..ed0342b9430 --- /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 e1bc8dc3228..67461d1a3e8 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 00000000000..e2360c016ca --- /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 00000000000..7622ac1b459 --- /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 5da2d4f9ef9..e7a438cae11 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 00000000000..02ac8ecf27a --- /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 00000000000..86e5d9051b3 --- /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 3741b035d7a..899b532290e 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 53b794ed435..0d0315e49c7 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 e447e89644f..32a378b7c00 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 2025bad6246..d9b65b6d1da 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 0539f5607b4..1db6712142d 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 8c2526f8a15..c40b7822073 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 5285ed21873..80b546ebcf8 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 05b0ffc0c44..4fa1f62c602 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 00000000000..7c129cba3af --- /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 c21a47c7ea2..9041f018423 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 6366f82b3da..5887460a8a5 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 76802a96367..d7451bd81a0 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 35000bf4e1f..7084c4609c5 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 498afcbb10a..5e0b881af15 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 286efcf0353..cfbedd994af 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 8d41ec1d5de..3351f0d5d8a 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 00000000000..372e37133b1 --- /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 00000000000..c1c8830cb25 --- /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 00000000000..7d8b268367c --- /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 ad553def6c3..3bdaf85498b 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 0b800ecd80a..39f254ac9af 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 280c941b427..9ffd5211a1f 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 48328aed878..f8b78af5721 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 00000000000..ad2c240baf1 --- /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 390069b7698..18c4c3c9ff2 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 dc408035d0f..6a017e9e1c3 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 ebd9d44796c..8d1c737170f 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 a64b4db185d..ed2e20f3527 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 5bd5b7ea765..2ded4cde472 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 f2be849cbc7..9acca6f0cd6 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 f28a4f59d80..c781e785f5e 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 d38b69f2248..ae5cd4bcdd9 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 7aa5090688c..c9869b25530 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 69c6ded5806..03a0c13dfce 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 e35a83bd24d..8a33baabc29 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 b8406c39711..f3839a1be55 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 7cbe3dc1cd4..0aa6af0d7c0 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 a22689f92bb..9f2480f9094 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 da77910097b..697d0a8b864 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 59d9508ac33..9790fbffd06 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 c9869b25530..7a8ce0780b7 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 58e5558781a..67a66bc9612 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 04a68bf9633..19acae64b16 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 7a8ce0780b7..44c10f8ff29 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 03a0c13dfce..2a8f51cd195 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 2ded4cde472..c5b566e5084 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)